@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/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",
@@ -1348,7 +1349,7 @@ function normalizeCheck(input, index) {
1348
1349
  throw new Error(`checks[${index}].type ${type} is not supported. Supported checks: ${RIDDLE_PROOF_PROFILE_CHECK_TYPES.join(", ")}`);
1349
1350
  }
1350
1351
  const isDialogCountCheck = isDialogCountCheckType(type);
1351
- 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)) {
1352
1353
  throw new Error(`checks[${index}] ${type} requires selector.`);
1353
1354
  }
1354
1355
  if ((type === "frame_text_visible" || type === "frame_url_equals" || type === "frame_url_matches" || type === "frame_no_horizontal_overflow") && !stringValue(input.selector)) {
@@ -1489,7 +1490,7 @@ function normalizeCheck(input, index) {
1489
1490
  allowed_content_types: allowedContentTypes,
1490
1491
  allow_get_fallback: isLinkStatusCheck ? input.allow_get_fallback === false || input.allowGetFallback === false ? false : true : void 0,
1491
1492
  max_overflow_px: numberValue(input.max_overflow_px),
1492
- 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),
1493
1494
  run_direct_routes: input.run_direct_routes === false || input.runDirectRoutes === false ? false : true,
1494
1495
  run_clickthroughs: input.run_clickthroughs === false || input.runClickthroughs === false ? false : true,
1495
1496
  run_all_viewports: input.run_all_viewports === true || input.runAllViewports === true,
@@ -2007,6 +2008,16 @@ function summarizeLinkStatusEvidence(viewport, check) {
2007
2008
  function textKey(check) {
2008
2009
  return check.pattern ? `pattern:${check.pattern}/${check.flags || ""}` : `text:${check.text || ""}`;
2009
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
+ }
2010
2021
  function textSequenceForCheck(viewport, check) {
2011
2022
  const key = selectorKey(check);
2012
2023
  const sequence = viewport.text_sequences?.[key];
@@ -2513,6 +2524,40 @@ function assessCheckFromEvidence(check, evidence) {
2513
2524
  message: failed ? `Selector ${key} text order failed in ${failed} viewport(s).` : void 0
2514
2525
  };
2515
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
+ }
2516
2561
  if (check.type === "frame_text_visible") {
2517
2562
  const key = selectorKey(check);
2518
2563
  const results = viewports.map((viewport) => {
@@ -4422,6 +4467,36 @@ function assessProfile(profile, evidence) {
4422
4467
  });
4423
4468
  continue;
4424
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
+ }
4425
4500
  if (check.type === "frame_text_visible") {
4426
4501
  const selector = check.selector || "";
4427
4502
  const results = checkViewports.map((viewport) => {
@@ -4854,6 +4929,19 @@ function ensureDialogHandler() {
4854
4929
  function textKey(check) {
4855
4930
  return check.pattern ? "pattern:" + check.pattern + "/" + (check.flags || "") : "text:" + (check.text || "");
4856
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
+ }
4857
4945
  function textMatches(sample, check) {
4858
4946
  if (check.pattern) {
4859
4947
  try { return new RegExp(check.pattern, check.flags || "").test(sample || ""); } catch { return false; }
@@ -6169,6 +6257,104 @@ async function selectorTextSequence(selector) {
6169
6257
  };
6170
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) }));
6171
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
+ }
6172
6358
  function linkProbeMaxLinks(check) {
6173
6359
  const value = Number(check.max_links || check.maxLinks || check.limit || 100);
6174
6360
  return Number.isInteger(value) && value > 0 ? Math.min(value, 500) : 100;
@@ -7221,6 +7407,7 @@ async function captureViewport(viewport) {
7221
7407
  const text_matches = {};
7222
7408
  const text_match_samples = {};
7223
7409
  const text_case_insensitive_samples = {};
7410
+ const observations = {};
7224
7411
  const http_statuses = {};
7225
7412
  const link_statuses = {};
7226
7413
  for (const check of profile.checks || []) {
@@ -7241,6 +7428,10 @@ async function captureViewport(viewport) {
7241
7428
  selectors[check.selector] = selectors[check.selector] || await selectorStats(check.selector);
7242
7429
  text_sequences[check.selector] = await selectorTextSequence(check.selector);
7243
7430
  }
7431
+ if (check.type === "observe_within") {
7432
+ const key = observeWithinKey(check);
7433
+ observations[key] = observations[key] || await observeWithin(check);
7434
+ }
7244
7435
  if ((check.type === "text_visible" || check.type === "text_absent") && (check.text || check.pattern)) {
7245
7436
  const key = textKey(check);
7246
7437
  const sample = dom.body_text || dom.body_text_sample || "";
@@ -7334,6 +7525,7 @@ async function captureViewport(viewport) {
7334
7525
  text_matches,
7335
7526
  text_match_samples,
7336
7527
  text_case_insensitive_samples,
7528
+ observations,
7337
7529
  http_statuses,
7338
7530
  link_statuses,
7339
7531
  route_inventory: routeInventory,