@riddledc/riddle-proof 0.7.41 → 0.7.43

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
@@ -145,6 +145,8 @@ or as a stronger proof base before a change loop.
145
145
  "checks": [
146
146
  { "type": "route_loaded", "expected_path": "/pricing" },
147
147
  { "type": "selector_visible", "selector": "[data-testid='pricing-cards']" },
148
+ { "type": "selector_absent", "selector": "[data-testid='loading-spinner']" },
149
+ { "type": "selector_count_equals", "selector": "[data-testid='pricing-card']", "expected_count": 3 },
148
150
  { "type": "text_visible", "text": "Start building" },
149
151
  { "type": "no_mobile_horizontal_overflow" },
150
152
  { "type": "no_fatal_console_errors" }
@@ -250,7 +252,25 @@ known browser console error, such as a mocked `503` that the app recovers from:
250
252
  Allowed console events and page errors are still counted in check evidence, but
251
253
  only unallowed `error` / `assert` console events and page errors fail the check.
252
254
  Use `allowed_page_error_patterns`, `allowed_console_texts`, or
253
- `allowed_page_error_texts` for narrower matching when needed.
255
+ `allowed_page_error_texts` for narrower matching when needed. Console allowlists
256
+ match both the console text and the event location URL, which is useful for
257
+ expected resource probes where the browser message is generic but the URL is
258
+ specific.
259
+
260
+ Use `selector_absent` when a forbidden element must not render, and
261
+ `selector_count_equals` / `selector_count_equal` / `selector_count_eq` when a
262
+ profile needs an exact DOM count rather than a lower bound:
263
+
264
+ ```json
265
+ [
266
+ { "type": "selector_absent", "selector": ".game-player-root iframe" },
267
+ { "type": "selector_count_equals", "selector": ".pricing-card", "expected_count": 3 }
268
+ ]
269
+ ```
270
+
271
+ These checks are useful for audit/no-diff profiles where the product should
272
+ show a fallback state and avoid rendering stale loaders, duplicate rows, or
273
+ missing-resource iframes.
254
274
 
255
275
  Use `selector_text_order` when a table, list, or card group must show visible
256
276
  items in a specific order after setup actions such as sorting or filtering:
@@ -13,7 +13,11 @@ var RIDDLE_PROOF_PROFILE_STATUSES = [
13
13
  var RIDDLE_PROOF_PROFILE_CHECK_TYPES = [
14
14
  "route_loaded",
15
15
  "selector_visible",
16
+ "selector_absent",
16
17
  "selector_count_at_least",
18
+ "selector_count_equals",
19
+ "selector_count_equal",
20
+ "selector_count_eq",
17
21
  "selector_text_order",
18
22
  "frame_text_visible",
19
23
  "frame_no_horizontal_overflow",
@@ -353,7 +357,7 @@ function normalizeCheck(input, index) {
353
357
  if (!isSupportedCheckType(type)) {
354
358
  throw new Error(`checks[${index}].type ${type} is not supported. Supported checks: ${RIDDLE_PROOF_PROFILE_CHECK_TYPES.join(", ")}`);
355
359
  }
356
- if ((type === "selector_visible" || type === "selector_count_at_least") && !stringValue(input.selector)) {
360
+ if ((type === "selector_visible" || type === "selector_absent" || type === "selector_count_at_least" || type === "selector_count_equals" || type === "selector_count_equal" || type === "selector_count_eq") && !stringValue(input.selector)) {
357
361
  throw new Error(`checks[${index}] ${type} requires selector.`);
358
362
  }
359
363
  if ((type === "frame_text_visible" || type === "frame_no_horizontal_overflow") && !stringValue(input.selector)) {
@@ -368,6 +372,10 @@ function normalizeCheck(input, index) {
368
372
  if (type === "selector_count_at_least" && numberValue(input.min_count) === void 0) {
369
373
  throw new Error(`checks[${index}] selector_count_at_least requires min_count.`);
370
374
  }
375
+ const expectedCount = numberValue(input.expected_count) ?? numberValue(input.expectedCount) ?? numberValue(input.count);
376
+ if ((type === "selector_count_equals" || type === "selector_count_equal" || type === "selector_count_eq") && expectedCount === void 0) {
377
+ throw new Error(`checks[${index}] ${type} requires expected_count.`);
378
+ }
371
379
  const expectedTexts = normalizeExpectedTexts(input.expected_texts ?? input.expectedTexts, index);
372
380
  if (type === "selector_text_order") {
373
381
  if (!stringValue(input.selector)) throw new Error(`checks[${index}] selector_text_order requires selector.`);
@@ -399,6 +407,7 @@ function normalizeCheck(input, index) {
399
407
  allowed_page_error_texts: normalizeStringList(input.allowed_page_error_texts ?? input.allowedPageErrorTexts ?? input.allow_page_error_texts ?? input.allowPageErrorTexts, `checks[${index}] allowed_page_error_texts`),
400
408
  allowed_page_error_patterns: normalizeStringList(input.allowed_page_error_patterns ?? input.allowedPageErrorPatterns ?? input.allow_page_error_patterns ?? input.allowPageErrorPatterns, `checks[${index}] allowed_page_error_patterns`),
401
409
  min_count: numberValue(input.min_count),
410
+ expected_count: expectedCount,
402
411
  max_overflow_px: numberValue(input.max_overflow_px),
403
412
  timeout_ms: numberValue(input.timeout_ms) ?? numberValue(input.timeoutMs),
404
413
  run_direct_routes: input.run_direct_routes === false || input.runDirectRoutes === false ? false : true,
@@ -561,8 +570,17 @@ function matchText(sample, check) {
561
570
  }
562
571
  return sample.includes(check.text || "");
563
572
  }
564
- function matchesAllowedMessage(message, texts, patterns) {
565
- const sample = String(message || "");
573
+ function allowedMessageSample(input) {
574
+ if (!isRecord(input)) return String(input || "");
575
+ const parts = [
576
+ input.text,
577
+ input.message,
578
+ isRecord(input.location) ? input.location.url : void 0
579
+ ];
580
+ return parts.map((part) => String(part || "")).filter(Boolean).join(" ");
581
+ }
582
+ function matchesAllowedMessage(input, texts, patterns) {
583
+ const sample = allowedMessageSample(input);
566
584
  if (texts?.some((text) => sample.includes(text))) return true;
567
585
  for (const pattern of patterns || []) {
568
586
  try {
@@ -666,6 +684,20 @@ function assessCheckFromEvidence(check, evidence) {
666
684
  message: failed.length ? `Selector ${key} was not visible in ${failed.length} viewport(s).` : void 0
667
685
  };
668
686
  }
687
+ if (check.type === "selector_absent") {
688
+ const key = selectorKey(check);
689
+ const failed = viewports.filter((viewport) => (viewport.selectors?.[key]?.count || 0) > 0);
690
+ return {
691
+ type: check.type,
692
+ label: checkLabel(check),
693
+ status: failed.length ? "failed" : "passed",
694
+ evidence: {
695
+ selector: key,
696
+ counts: viewports.map((viewport) => viewport.selectors?.[key]?.count || 0)
697
+ },
698
+ message: failed.length ? `Selector ${key} was present in ${failed.length} viewport(s).` : void 0
699
+ };
700
+ }
669
701
  if (check.type === "selector_count_at_least") {
670
702
  const key = selectorKey(check);
671
703
  const minCount = check.min_count ?? 1;
@@ -682,6 +714,22 @@ function assessCheckFromEvidence(check, evidence) {
682
714
  message: failed.length ? `Selector ${key} count was below ${minCount} in ${failed.length} viewport(s).` : void 0
683
715
  };
684
716
  }
717
+ if (check.type === "selector_count_equals" || check.type === "selector_count_equal" || check.type === "selector_count_eq") {
718
+ const key = selectorKey(check);
719
+ const expectedCount = check.expected_count ?? 0;
720
+ const failed = viewports.filter((viewport) => (viewport.selectors?.[key]?.count || 0) !== expectedCount);
721
+ return {
722
+ type: check.type,
723
+ label: checkLabel(check),
724
+ status: failed.length ? "failed" : "passed",
725
+ evidence: {
726
+ selector: key,
727
+ expected_count: expectedCount,
728
+ counts: viewports.map((viewport) => viewport.selectors?.[key]?.count || 0)
729
+ },
730
+ message: failed.length ? `Selector ${key} count did not equal ${expectedCount} in ${failed.length} viewport(s).` : void 0
731
+ };
732
+ }
685
733
  if (check.type === "selector_text_order") {
686
734
  const key = selectorKey(check);
687
735
  const expectedTexts = check.expected_texts || [];
@@ -860,11 +908,11 @@ function assessCheckFromEvidence(check, evidence) {
860
908
  }
861
909
  if (check.type === "no_fatal_console_errors") {
862
910
  const fatalConsoleEvents = (evidence.console?.events || []).filter((event) => event.type === "error" || event.type === "assert");
863
- const allowedConsoleEvents = fatalConsoleEvents.filter((event) => matchesAllowedMessage(event.text, check.allowed_console_texts, check.allowed_console_patterns));
864
- const unallowedConsoleEvents = fatalConsoleEvents.filter((event) => !matchesAllowedMessage(event.text, check.allowed_console_texts, check.allowed_console_patterns));
911
+ const allowedConsoleEvents = fatalConsoleEvents.filter((event) => matchesAllowedMessage(event, check.allowed_console_texts, check.allowed_console_patterns));
912
+ const unallowedConsoleEvents = fatalConsoleEvents.filter((event) => !matchesAllowedMessage(event, check.allowed_console_texts, check.allowed_console_patterns));
865
913
  const pageErrors = evidence.page_errors || [];
866
- const allowedPageErrors = pageErrors.filter((error) => matchesAllowedMessage(error.message, check.allowed_page_error_texts, check.allowed_page_error_patterns));
867
- const unallowedPageErrors = pageErrors.filter((error) => !matchesAllowedMessage(error.message, check.allowed_page_error_texts, check.allowed_page_error_patterns));
914
+ const allowedPageErrors = pageErrors.filter((error) => matchesAllowedMessage(error, check.allowed_page_error_texts, check.allowed_page_error_patterns));
915
+ const unallowedPageErrors = pageErrors.filter((error) => !matchesAllowedMessage(error, check.allowed_page_error_texts, check.allowed_page_error_patterns));
868
916
  const fatalCount = unallowedConsoleEvents.length + unallowedPageErrors.length;
869
917
  return {
870
918
  type: check.type,
@@ -1149,8 +1197,17 @@ function textMatches(sample, check) {
1149
1197
  }
1150
1198
  return String(sample || "").includes(check.text || "");
1151
1199
  }
1152
- function matchesAllowedMessage(message, texts, patterns) {
1153
- const sample = String(message || "");
1200
+ function allowedMessageSample(input) {
1201
+ if (!input || typeof input !== "object" || Array.isArray(input)) return String(input || "");
1202
+ const parts = [
1203
+ input.text,
1204
+ input.message,
1205
+ input.location && typeof input.location === "object" ? input.location.url : undefined,
1206
+ ];
1207
+ return parts.map((part) => String(part || "")).filter(Boolean).join(" ");
1208
+ }
1209
+ function matchesAllowedMessage(input, texts, patterns) {
1210
+ const sample = allowedMessageSample(input);
1154
1211
  if ((texts || []).some((text) => sample.includes(text))) return true;
1155
1212
  for (const pattern of patterns || []) {
1156
1213
  try {
@@ -1418,6 +1475,18 @@ function assessProfile(profile, evidence) {
1418
1475
  });
1419
1476
  continue;
1420
1477
  }
1478
+ if (check.type === "selector_absent") {
1479
+ const selector = check.selector || "";
1480
+ const failed = viewports.filter((viewport) => viewport.selectors && viewport.selectors[selector] && viewport.selectors[selector].count > 0);
1481
+ checks.push({
1482
+ type: check.type,
1483
+ label: check.label || check.type,
1484
+ status: failed.length ? "failed" : "passed",
1485
+ evidence: { selector, counts: viewports.map((viewport) => viewport.selectors && viewport.selectors[selector] ? viewport.selectors[selector].count : 0) },
1486
+ message: failed.length ? "Selector " + selector + " was present in " + failed.length + " viewport(s)." : undefined,
1487
+ });
1488
+ continue;
1489
+ }
1421
1490
  if (check.type === "selector_count_at_least") {
1422
1491
  const selector = check.selector || "";
1423
1492
  const minCount = check.min_count == null ? 1 : check.min_count;
@@ -1431,6 +1500,19 @@ function assessProfile(profile, evidence) {
1431
1500
  });
1432
1501
  continue;
1433
1502
  }
1503
+ if (check.type === "selector_count_equals" || check.type === "selector_count_equal" || check.type === "selector_count_eq") {
1504
+ const selector = check.selector || "";
1505
+ const expectedCount = check.expected_count == null ? 0 : check.expected_count;
1506
+ const failed = viewports.filter((viewport) => (viewport.selectors && viewport.selectors[selector] ? viewport.selectors[selector].count : 0) !== expectedCount);
1507
+ checks.push({
1508
+ type: check.type,
1509
+ label: check.label || check.type,
1510
+ status: failed.length ? "failed" : "passed",
1511
+ evidence: { selector, expected_count: expectedCount, counts: viewports.map((viewport) => viewport.selectors && viewport.selectors[selector] ? viewport.selectors[selector].count : 0) },
1512
+ message: failed.length ? "Selector " + selector + " count did not equal " + expectedCount + " in " + failed.length + " viewport(s)." : undefined,
1513
+ });
1514
+ continue;
1515
+ }
1434
1516
  if (check.type === "selector_text_order") {
1435
1517
  const selector = check.selector || "";
1436
1518
  const expectedTexts = check.expected_texts || [];
@@ -1606,11 +1688,11 @@ function assessProfile(profile, evidence) {
1606
1688
  }
1607
1689
  if (check.type === "no_fatal_console_errors") {
1608
1690
  const fatalConsoleEvents = ((evidence.console && evidence.console.events) || []).filter((event) => event && (event.type === "error" || event.type === "assert"));
1609
- const allowedConsoleEvents = fatalConsoleEvents.filter((event) => matchesAllowedMessage(event.text, check.allowed_console_texts, check.allowed_console_patterns));
1610
- const unallowedConsoleEvents = fatalConsoleEvents.filter((event) => !matchesAllowedMessage(event.text, check.allowed_console_texts, check.allowed_console_patterns));
1691
+ const allowedConsoleEvents = fatalConsoleEvents.filter((event) => matchesAllowedMessage(event, check.allowed_console_texts, check.allowed_console_patterns));
1692
+ const unallowedConsoleEvents = fatalConsoleEvents.filter((event) => !matchesAllowedMessage(event, check.allowed_console_texts, check.allowed_console_patterns));
1611
1693
  const pageErrors = evidence.page_errors || [];
1612
- const allowedPageErrors = pageErrors.filter((error) => matchesAllowedMessage(error && error.message, check.allowed_page_error_texts, check.allowed_page_error_patterns));
1613
- const unallowedPageErrors = pageErrors.filter((error) => !matchesAllowedMessage(error && error.message, check.allowed_page_error_texts, check.allowed_page_error_patterns));
1694
+ const allowedPageErrors = pageErrors.filter((error) => matchesAllowedMessage(error, check.allowed_page_error_texts, check.allowed_page_error_patterns));
1695
+ const unallowedPageErrors = pageErrors.filter((error) => !matchesAllowedMessage(error, check.allowed_page_error_texts, check.allowed_page_error_patterns));
1614
1696
  const fatalCount = unallowedConsoleEvents.length + unallowedPageErrors.length;
1615
1697
  checks.push({
1616
1698
  type: check.type,
@@ -2447,7 +2529,16 @@ async function captureViewport(viewport) {
2447
2529
  const text_sequences = {};
2448
2530
  const text_matches = {};
2449
2531
  for (const check of profile.checks || []) {
2450
- if ((check.type === "selector_visible" || check.type === "selector_count_at_least") && check.selector) {
2532
+ if (
2533
+ (
2534
+ check.type === "selector_visible"
2535
+ || check.type === "selector_absent"
2536
+ || check.type === "selector_count_at_least"
2537
+ || check.type === "selector_count_equals"
2538
+ || check.type === "selector_count_equal"
2539
+ || check.type === "selector_count_eq"
2540
+ ) && check.selector
2541
+ ) {
2451
2542
  selectors[check.selector] = await selectorStats(check.selector);
2452
2543
  }
2453
2544
  if (check.type === "selector_text_order" && check.selector) {
package/dist/cli.cjs CHANGED
@@ -6886,7 +6886,11 @@ var RIDDLE_PROOF_PROFILE_STATUSES = [
6886
6886
  var RIDDLE_PROOF_PROFILE_CHECK_TYPES = [
6887
6887
  "route_loaded",
6888
6888
  "selector_visible",
6889
+ "selector_absent",
6889
6890
  "selector_count_at_least",
6891
+ "selector_count_equals",
6892
+ "selector_count_equal",
6893
+ "selector_count_eq",
6890
6894
  "selector_text_order",
6891
6895
  "frame_text_visible",
6892
6896
  "frame_no_horizontal_overflow",
@@ -7226,7 +7230,7 @@ function normalizeCheck(input, index) {
7226
7230
  if (!isSupportedCheckType(type)) {
7227
7231
  throw new Error(`checks[${index}].type ${type} is not supported. Supported checks: ${RIDDLE_PROOF_PROFILE_CHECK_TYPES.join(", ")}`);
7228
7232
  }
7229
- if ((type === "selector_visible" || type === "selector_count_at_least") && !stringValue2(input.selector)) {
7233
+ if ((type === "selector_visible" || type === "selector_absent" || type === "selector_count_at_least" || type === "selector_count_equals" || type === "selector_count_equal" || type === "selector_count_eq") && !stringValue2(input.selector)) {
7230
7234
  throw new Error(`checks[${index}] ${type} requires selector.`);
7231
7235
  }
7232
7236
  if ((type === "frame_text_visible" || type === "frame_no_horizontal_overflow") && !stringValue2(input.selector)) {
@@ -7241,6 +7245,10 @@ function normalizeCheck(input, index) {
7241
7245
  if (type === "selector_count_at_least" && numberValue(input.min_count) === void 0) {
7242
7246
  throw new Error(`checks[${index}] selector_count_at_least requires min_count.`);
7243
7247
  }
7248
+ const expectedCount = numberValue(input.expected_count) ?? numberValue(input.expectedCount) ?? numberValue(input.count);
7249
+ if ((type === "selector_count_equals" || type === "selector_count_equal" || type === "selector_count_eq") && expectedCount === void 0) {
7250
+ throw new Error(`checks[${index}] ${type} requires expected_count.`);
7251
+ }
7244
7252
  const expectedTexts = normalizeExpectedTexts(input.expected_texts ?? input.expectedTexts, index);
7245
7253
  if (type === "selector_text_order") {
7246
7254
  if (!stringValue2(input.selector)) throw new Error(`checks[${index}] selector_text_order requires selector.`);
@@ -7272,6 +7280,7 @@ function normalizeCheck(input, index) {
7272
7280
  allowed_page_error_texts: normalizeStringList(input.allowed_page_error_texts ?? input.allowedPageErrorTexts ?? input.allow_page_error_texts ?? input.allowPageErrorTexts, `checks[${index}] allowed_page_error_texts`),
7273
7281
  allowed_page_error_patterns: normalizeStringList(input.allowed_page_error_patterns ?? input.allowedPageErrorPatterns ?? input.allow_page_error_patterns ?? input.allowPageErrorPatterns, `checks[${index}] allowed_page_error_patterns`),
7274
7282
  min_count: numberValue(input.min_count),
7283
+ expected_count: expectedCount,
7275
7284
  max_overflow_px: numberValue(input.max_overflow_px),
7276
7285
  timeout_ms: numberValue(input.timeout_ms) ?? numberValue(input.timeoutMs),
7277
7286
  run_direct_routes: input.run_direct_routes === false || input.runDirectRoutes === false ? false : true,
@@ -7434,8 +7443,17 @@ function matchText(sample, check) {
7434
7443
  }
7435
7444
  return sample.includes(check.text || "");
7436
7445
  }
7437
- function matchesAllowedMessage(message, texts, patterns) {
7438
- const sample = String(message || "");
7446
+ function allowedMessageSample(input) {
7447
+ if (!isRecord(input)) return String(input || "");
7448
+ const parts = [
7449
+ input.text,
7450
+ input.message,
7451
+ isRecord(input.location) ? input.location.url : void 0
7452
+ ];
7453
+ return parts.map((part) => String(part || "")).filter(Boolean).join(" ");
7454
+ }
7455
+ function matchesAllowedMessage(input, texts, patterns) {
7456
+ const sample = allowedMessageSample(input);
7439
7457
  if (texts?.some((text) => sample.includes(text))) return true;
7440
7458
  for (const pattern of patterns || []) {
7441
7459
  try {
@@ -7539,6 +7557,20 @@ function assessCheckFromEvidence(check, evidence) {
7539
7557
  message: failed.length ? `Selector ${key} was not visible in ${failed.length} viewport(s).` : void 0
7540
7558
  };
7541
7559
  }
7560
+ if (check.type === "selector_absent") {
7561
+ const key = selectorKey(check);
7562
+ const failed = viewports.filter((viewport) => (viewport.selectors?.[key]?.count || 0) > 0);
7563
+ return {
7564
+ type: check.type,
7565
+ label: checkLabel(check),
7566
+ status: failed.length ? "failed" : "passed",
7567
+ evidence: {
7568
+ selector: key,
7569
+ counts: viewports.map((viewport) => viewport.selectors?.[key]?.count || 0)
7570
+ },
7571
+ message: failed.length ? `Selector ${key} was present in ${failed.length} viewport(s).` : void 0
7572
+ };
7573
+ }
7542
7574
  if (check.type === "selector_count_at_least") {
7543
7575
  const key = selectorKey(check);
7544
7576
  const minCount = check.min_count ?? 1;
@@ -7555,6 +7587,22 @@ function assessCheckFromEvidence(check, evidence) {
7555
7587
  message: failed.length ? `Selector ${key} count was below ${minCount} in ${failed.length} viewport(s).` : void 0
7556
7588
  };
7557
7589
  }
7590
+ if (check.type === "selector_count_equals" || check.type === "selector_count_equal" || check.type === "selector_count_eq") {
7591
+ const key = selectorKey(check);
7592
+ const expectedCount = check.expected_count ?? 0;
7593
+ const failed = viewports.filter((viewport) => (viewport.selectors?.[key]?.count || 0) !== expectedCount);
7594
+ return {
7595
+ type: check.type,
7596
+ label: checkLabel(check),
7597
+ status: failed.length ? "failed" : "passed",
7598
+ evidence: {
7599
+ selector: key,
7600
+ expected_count: expectedCount,
7601
+ counts: viewports.map((viewport) => viewport.selectors?.[key]?.count || 0)
7602
+ },
7603
+ message: failed.length ? `Selector ${key} count did not equal ${expectedCount} in ${failed.length} viewport(s).` : void 0
7604
+ };
7605
+ }
7558
7606
  if (check.type === "selector_text_order") {
7559
7607
  const key = selectorKey(check);
7560
7608
  const expectedTexts = check.expected_texts || [];
@@ -7733,11 +7781,11 @@ function assessCheckFromEvidence(check, evidence) {
7733
7781
  }
7734
7782
  if (check.type === "no_fatal_console_errors") {
7735
7783
  const fatalConsoleEvents = (evidence.console?.events || []).filter((event) => event.type === "error" || event.type === "assert");
7736
- const allowedConsoleEvents = fatalConsoleEvents.filter((event) => matchesAllowedMessage(event.text, check.allowed_console_texts, check.allowed_console_patterns));
7737
- const unallowedConsoleEvents = fatalConsoleEvents.filter((event) => !matchesAllowedMessage(event.text, check.allowed_console_texts, check.allowed_console_patterns));
7784
+ const allowedConsoleEvents = fatalConsoleEvents.filter((event) => matchesAllowedMessage(event, check.allowed_console_texts, check.allowed_console_patterns));
7785
+ const unallowedConsoleEvents = fatalConsoleEvents.filter((event) => !matchesAllowedMessage(event, check.allowed_console_texts, check.allowed_console_patterns));
7738
7786
  const pageErrors = evidence.page_errors || [];
7739
- const allowedPageErrors = pageErrors.filter((error) => matchesAllowedMessage(error.message, check.allowed_page_error_texts, check.allowed_page_error_patterns));
7740
- const unallowedPageErrors = pageErrors.filter((error) => !matchesAllowedMessage(error.message, check.allowed_page_error_texts, check.allowed_page_error_patterns));
7787
+ const allowedPageErrors = pageErrors.filter((error) => matchesAllowedMessage(error, check.allowed_page_error_texts, check.allowed_page_error_patterns));
7788
+ const unallowedPageErrors = pageErrors.filter((error) => !matchesAllowedMessage(error, check.allowed_page_error_texts, check.allowed_page_error_patterns));
7741
7789
  const fatalCount = unallowedConsoleEvents.length + unallowedPageErrors.length;
7742
7790
  return {
7743
7791
  type: check.type,
@@ -8006,8 +8054,17 @@ function textMatches(sample, check) {
8006
8054
  }
8007
8055
  return String(sample || "").includes(check.text || "");
8008
8056
  }
8009
- function matchesAllowedMessage(message, texts, patterns) {
8010
- const sample = String(message || "");
8057
+ function allowedMessageSample(input) {
8058
+ if (!input || typeof input !== "object" || Array.isArray(input)) return String(input || "");
8059
+ const parts = [
8060
+ input.text,
8061
+ input.message,
8062
+ input.location && typeof input.location === "object" ? input.location.url : undefined,
8063
+ ];
8064
+ return parts.map((part) => String(part || "")).filter(Boolean).join(" ");
8065
+ }
8066
+ function matchesAllowedMessage(input, texts, patterns) {
8067
+ const sample = allowedMessageSample(input);
8011
8068
  if ((texts || []).some((text) => sample.includes(text))) return true;
8012
8069
  for (const pattern of patterns || []) {
8013
8070
  try {
@@ -8275,6 +8332,18 @@ function assessProfile(profile, evidence) {
8275
8332
  });
8276
8333
  continue;
8277
8334
  }
8335
+ if (check.type === "selector_absent") {
8336
+ const selector = check.selector || "";
8337
+ const failed = viewports.filter((viewport) => viewport.selectors && viewport.selectors[selector] && viewport.selectors[selector].count > 0);
8338
+ checks.push({
8339
+ type: check.type,
8340
+ label: check.label || check.type,
8341
+ status: failed.length ? "failed" : "passed",
8342
+ evidence: { selector, counts: viewports.map((viewport) => viewport.selectors && viewport.selectors[selector] ? viewport.selectors[selector].count : 0) },
8343
+ message: failed.length ? "Selector " + selector + " was present in " + failed.length + " viewport(s)." : undefined,
8344
+ });
8345
+ continue;
8346
+ }
8278
8347
  if (check.type === "selector_count_at_least") {
8279
8348
  const selector = check.selector || "";
8280
8349
  const minCount = check.min_count == null ? 1 : check.min_count;
@@ -8288,6 +8357,19 @@ function assessProfile(profile, evidence) {
8288
8357
  });
8289
8358
  continue;
8290
8359
  }
8360
+ if (check.type === "selector_count_equals" || check.type === "selector_count_equal" || check.type === "selector_count_eq") {
8361
+ const selector = check.selector || "";
8362
+ const expectedCount = check.expected_count == null ? 0 : check.expected_count;
8363
+ const failed = viewports.filter((viewport) => (viewport.selectors && viewport.selectors[selector] ? viewport.selectors[selector].count : 0) !== expectedCount);
8364
+ checks.push({
8365
+ type: check.type,
8366
+ label: check.label || check.type,
8367
+ status: failed.length ? "failed" : "passed",
8368
+ evidence: { selector, expected_count: expectedCount, counts: viewports.map((viewport) => viewport.selectors && viewport.selectors[selector] ? viewport.selectors[selector].count : 0) },
8369
+ message: failed.length ? "Selector " + selector + " count did not equal " + expectedCount + " in " + failed.length + " viewport(s)." : undefined,
8370
+ });
8371
+ continue;
8372
+ }
8291
8373
  if (check.type === "selector_text_order") {
8292
8374
  const selector = check.selector || "";
8293
8375
  const expectedTexts = check.expected_texts || [];
@@ -8463,11 +8545,11 @@ function assessProfile(profile, evidence) {
8463
8545
  }
8464
8546
  if (check.type === "no_fatal_console_errors") {
8465
8547
  const fatalConsoleEvents = ((evidence.console && evidence.console.events) || []).filter((event) => event && (event.type === "error" || event.type === "assert"));
8466
- const allowedConsoleEvents = fatalConsoleEvents.filter((event) => matchesAllowedMessage(event.text, check.allowed_console_texts, check.allowed_console_patterns));
8467
- const unallowedConsoleEvents = fatalConsoleEvents.filter((event) => !matchesAllowedMessage(event.text, check.allowed_console_texts, check.allowed_console_patterns));
8548
+ const allowedConsoleEvents = fatalConsoleEvents.filter((event) => matchesAllowedMessage(event, check.allowed_console_texts, check.allowed_console_patterns));
8549
+ const unallowedConsoleEvents = fatalConsoleEvents.filter((event) => !matchesAllowedMessage(event, check.allowed_console_texts, check.allowed_console_patterns));
8468
8550
  const pageErrors = evidence.page_errors || [];
8469
- const allowedPageErrors = pageErrors.filter((error) => matchesAllowedMessage(error && error.message, check.allowed_page_error_texts, check.allowed_page_error_patterns));
8470
- const unallowedPageErrors = pageErrors.filter((error) => !matchesAllowedMessage(error && error.message, check.allowed_page_error_texts, check.allowed_page_error_patterns));
8551
+ const allowedPageErrors = pageErrors.filter((error) => matchesAllowedMessage(error, check.allowed_page_error_texts, check.allowed_page_error_patterns));
8552
+ const unallowedPageErrors = pageErrors.filter((error) => !matchesAllowedMessage(error, check.allowed_page_error_texts, check.allowed_page_error_patterns));
8471
8553
  const fatalCount = unallowedConsoleEvents.length + unallowedPageErrors.length;
8472
8554
  checks.push({
8473
8555
  type: check.type,
@@ -9304,7 +9386,16 @@ async function captureViewport(viewport) {
9304
9386
  const text_sequences = {};
9305
9387
  const text_matches = {};
9306
9388
  for (const check of profile.checks || []) {
9307
- if ((check.type === "selector_visible" || check.type === "selector_count_at_least") && check.selector) {
9389
+ if (
9390
+ (
9391
+ check.type === "selector_visible"
9392
+ || check.type === "selector_absent"
9393
+ || check.type === "selector_count_at_least"
9394
+ || check.type === "selector_count_equals"
9395
+ || check.type === "selector_count_equal"
9396
+ || check.type === "selector_count_eq"
9397
+ ) && check.selector
9398
+ ) {
9308
9399
  selectors[check.selector] = await selectorStats(check.selector);
9309
9400
  }
9310
9401
  if (check.type === "selector_text_order" && check.selector) {
package/dist/cli.js CHANGED
@@ -10,7 +10,7 @@ import {
10
10
  profileStatusExitCode,
11
11
  resolveRiddleProofProfileTargetUrl,
12
12
  resolveRiddleProofProfileTimeoutSec
13
- } from "./chunk-6MVVU54I.js";
13
+ } from "./chunk-ZB5DSQF2.js";
14
14
  import {
15
15
  createRiddleApiClient,
16
16
  parseRiddleViewport
package/dist/index.cjs CHANGED
@@ -8727,7 +8727,11 @@ var RIDDLE_PROOF_PROFILE_STATUSES = [
8727
8727
  var RIDDLE_PROOF_PROFILE_CHECK_TYPES = [
8728
8728
  "route_loaded",
8729
8729
  "selector_visible",
8730
+ "selector_absent",
8730
8731
  "selector_count_at_least",
8732
+ "selector_count_equals",
8733
+ "selector_count_equal",
8734
+ "selector_count_eq",
8731
8735
  "selector_text_order",
8732
8736
  "frame_text_visible",
8733
8737
  "frame_no_horizontal_overflow",
@@ -9067,7 +9071,7 @@ function normalizeCheck(input, index) {
9067
9071
  if (!isSupportedCheckType(type)) {
9068
9072
  throw new Error(`checks[${index}].type ${type} is not supported. Supported checks: ${RIDDLE_PROOF_PROFILE_CHECK_TYPES.join(", ")}`);
9069
9073
  }
9070
- if ((type === "selector_visible" || type === "selector_count_at_least") && !stringValue5(input.selector)) {
9074
+ if ((type === "selector_visible" || type === "selector_absent" || type === "selector_count_at_least" || type === "selector_count_equals" || type === "selector_count_equal" || type === "selector_count_eq") && !stringValue5(input.selector)) {
9071
9075
  throw new Error(`checks[${index}] ${type} requires selector.`);
9072
9076
  }
9073
9077
  if ((type === "frame_text_visible" || type === "frame_no_horizontal_overflow") && !stringValue5(input.selector)) {
@@ -9082,6 +9086,10 @@ function normalizeCheck(input, index) {
9082
9086
  if (type === "selector_count_at_least" && numberValue3(input.min_count) === void 0) {
9083
9087
  throw new Error(`checks[${index}] selector_count_at_least requires min_count.`);
9084
9088
  }
9089
+ const expectedCount = numberValue3(input.expected_count) ?? numberValue3(input.expectedCount) ?? numberValue3(input.count);
9090
+ if ((type === "selector_count_equals" || type === "selector_count_equal" || type === "selector_count_eq") && expectedCount === void 0) {
9091
+ throw new Error(`checks[${index}] ${type} requires expected_count.`);
9092
+ }
9085
9093
  const expectedTexts = normalizeExpectedTexts(input.expected_texts ?? input.expectedTexts, index);
9086
9094
  if (type === "selector_text_order") {
9087
9095
  if (!stringValue5(input.selector)) throw new Error(`checks[${index}] selector_text_order requires selector.`);
@@ -9113,6 +9121,7 @@ function normalizeCheck(input, index) {
9113
9121
  allowed_page_error_texts: normalizeStringList(input.allowed_page_error_texts ?? input.allowedPageErrorTexts ?? input.allow_page_error_texts ?? input.allowPageErrorTexts, `checks[${index}] allowed_page_error_texts`),
9114
9122
  allowed_page_error_patterns: normalizeStringList(input.allowed_page_error_patterns ?? input.allowedPageErrorPatterns ?? input.allow_page_error_patterns ?? input.allowPageErrorPatterns, `checks[${index}] allowed_page_error_patterns`),
9115
9123
  min_count: numberValue3(input.min_count),
9124
+ expected_count: expectedCount,
9116
9125
  max_overflow_px: numberValue3(input.max_overflow_px),
9117
9126
  timeout_ms: numberValue3(input.timeout_ms) ?? numberValue3(input.timeoutMs),
9118
9127
  run_direct_routes: input.run_direct_routes === false || input.runDirectRoutes === false ? false : true,
@@ -9275,8 +9284,17 @@ function matchText(sample, check) {
9275
9284
  }
9276
9285
  return sample.includes(check.text || "");
9277
9286
  }
9278
- function matchesAllowedMessage(message, texts, patterns) {
9279
- const sample = String(message || "");
9287
+ function allowedMessageSample(input) {
9288
+ if (!isRecord2(input)) return String(input || "");
9289
+ const parts = [
9290
+ input.text,
9291
+ input.message,
9292
+ isRecord2(input.location) ? input.location.url : void 0
9293
+ ];
9294
+ return parts.map((part) => String(part || "")).filter(Boolean).join(" ");
9295
+ }
9296
+ function matchesAllowedMessage(input, texts, patterns) {
9297
+ const sample = allowedMessageSample(input);
9280
9298
  if (texts?.some((text) => sample.includes(text))) return true;
9281
9299
  for (const pattern of patterns || []) {
9282
9300
  try {
@@ -9380,6 +9398,20 @@ function assessCheckFromEvidence(check, evidence) {
9380
9398
  message: failed.length ? `Selector ${key} was not visible in ${failed.length} viewport(s).` : void 0
9381
9399
  };
9382
9400
  }
9401
+ if (check.type === "selector_absent") {
9402
+ const key = selectorKey(check);
9403
+ const failed = viewports.filter((viewport) => (viewport.selectors?.[key]?.count || 0) > 0);
9404
+ return {
9405
+ type: check.type,
9406
+ label: checkLabel(check),
9407
+ status: failed.length ? "failed" : "passed",
9408
+ evidence: {
9409
+ selector: key,
9410
+ counts: viewports.map((viewport) => viewport.selectors?.[key]?.count || 0)
9411
+ },
9412
+ message: failed.length ? `Selector ${key} was present in ${failed.length} viewport(s).` : void 0
9413
+ };
9414
+ }
9383
9415
  if (check.type === "selector_count_at_least") {
9384
9416
  const key = selectorKey(check);
9385
9417
  const minCount = check.min_count ?? 1;
@@ -9396,6 +9428,22 @@ function assessCheckFromEvidence(check, evidence) {
9396
9428
  message: failed.length ? `Selector ${key} count was below ${minCount} in ${failed.length} viewport(s).` : void 0
9397
9429
  };
9398
9430
  }
9431
+ if (check.type === "selector_count_equals" || check.type === "selector_count_equal" || check.type === "selector_count_eq") {
9432
+ const key = selectorKey(check);
9433
+ const expectedCount = check.expected_count ?? 0;
9434
+ const failed = viewports.filter((viewport) => (viewport.selectors?.[key]?.count || 0) !== expectedCount);
9435
+ return {
9436
+ type: check.type,
9437
+ label: checkLabel(check),
9438
+ status: failed.length ? "failed" : "passed",
9439
+ evidence: {
9440
+ selector: key,
9441
+ expected_count: expectedCount,
9442
+ counts: viewports.map((viewport) => viewport.selectors?.[key]?.count || 0)
9443
+ },
9444
+ message: failed.length ? `Selector ${key} count did not equal ${expectedCount} in ${failed.length} viewport(s).` : void 0
9445
+ };
9446
+ }
9399
9447
  if (check.type === "selector_text_order") {
9400
9448
  const key = selectorKey(check);
9401
9449
  const expectedTexts = check.expected_texts || [];
@@ -9574,11 +9622,11 @@ function assessCheckFromEvidence(check, evidence) {
9574
9622
  }
9575
9623
  if (check.type === "no_fatal_console_errors") {
9576
9624
  const fatalConsoleEvents = (evidence.console?.events || []).filter((event) => event.type === "error" || event.type === "assert");
9577
- const allowedConsoleEvents = fatalConsoleEvents.filter((event) => matchesAllowedMessage(event.text, check.allowed_console_texts, check.allowed_console_patterns));
9578
- const unallowedConsoleEvents = fatalConsoleEvents.filter((event) => !matchesAllowedMessage(event.text, check.allowed_console_texts, check.allowed_console_patterns));
9625
+ const allowedConsoleEvents = fatalConsoleEvents.filter((event) => matchesAllowedMessage(event, check.allowed_console_texts, check.allowed_console_patterns));
9626
+ const unallowedConsoleEvents = fatalConsoleEvents.filter((event) => !matchesAllowedMessage(event, check.allowed_console_texts, check.allowed_console_patterns));
9579
9627
  const pageErrors = evidence.page_errors || [];
9580
- const allowedPageErrors = pageErrors.filter((error) => matchesAllowedMessage(error.message, check.allowed_page_error_texts, check.allowed_page_error_patterns));
9581
- const unallowedPageErrors = pageErrors.filter((error) => !matchesAllowedMessage(error.message, check.allowed_page_error_texts, check.allowed_page_error_patterns));
9628
+ const allowedPageErrors = pageErrors.filter((error) => matchesAllowedMessage(error, check.allowed_page_error_texts, check.allowed_page_error_patterns));
9629
+ const unallowedPageErrors = pageErrors.filter((error) => !matchesAllowedMessage(error, check.allowed_page_error_texts, check.allowed_page_error_patterns));
9582
9630
  const fatalCount = unallowedConsoleEvents.length + unallowedPageErrors.length;
9583
9631
  return {
9584
9632
  type: check.type,
@@ -9863,8 +9911,17 @@ function textMatches(sample, check) {
9863
9911
  }
9864
9912
  return String(sample || "").includes(check.text || "");
9865
9913
  }
9866
- function matchesAllowedMessage(message, texts, patterns) {
9867
- const sample = String(message || "");
9914
+ function allowedMessageSample(input) {
9915
+ if (!input || typeof input !== "object" || Array.isArray(input)) return String(input || "");
9916
+ const parts = [
9917
+ input.text,
9918
+ input.message,
9919
+ input.location && typeof input.location === "object" ? input.location.url : undefined,
9920
+ ];
9921
+ return parts.map((part) => String(part || "")).filter(Boolean).join(" ");
9922
+ }
9923
+ function matchesAllowedMessage(input, texts, patterns) {
9924
+ const sample = allowedMessageSample(input);
9868
9925
  if ((texts || []).some((text) => sample.includes(text))) return true;
9869
9926
  for (const pattern of patterns || []) {
9870
9927
  try {
@@ -10132,6 +10189,18 @@ function assessProfile(profile, evidence) {
10132
10189
  });
10133
10190
  continue;
10134
10191
  }
10192
+ if (check.type === "selector_absent") {
10193
+ const selector = check.selector || "";
10194
+ const failed = viewports.filter((viewport) => viewport.selectors && viewport.selectors[selector] && viewport.selectors[selector].count > 0);
10195
+ checks.push({
10196
+ type: check.type,
10197
+ label: check.label || check.type,
10198
+ status: failed.length ? "failed" : "passed",
10199
+ evidence: { selector, counts: viewports.map((viewport) => viewport.selectors && viewport.selectors[selector] ? viewport.selectors[selector].count : 0) },
10200
+ message: failed.length ? "Selector " + selector + " was present in " + failed.length + " viewport(s)." : undefined,
10201
+ });
10202
+ continue;
10203
+ }
10135
10204
  if (check.type === "selector_count_at_least") {
10136
10205
  const selector = check.selector || "";
10137
10206
  const minCount = check.min_count == null ? 1 : check.min_count;
@@ -10145,6 +10214,19 @@ function assessProfile(profile, evidence) {
10145
10214
  });
10146
10215
  continue;
10147
10216
  }
10217
+ if (check.type === "selector_count_equals" || check.type === "selector_count_equal" || check.type === "selector_count_eq") {
10218
+ const selector = check.selector || "";
10219
+ const expectedCount = check.expected_count == null ? 0 : check.expected_count;
10220
+ const failed = viewports.filter((viewport) => (viewport.selectors && viewport.selectors[selector] ? viewport.selectors[selector].count : 0) !== expectedCount);
10221
+ checks.push({
10222
+ type: check.type,
10223
+ label: check.label || check.type,
10224
+ status: failed.length ? "failed" : "passed",
10225
+ evidence: { selector, expected_count: expectedCount, counts: viewports.map((viewport) => viewport.selectors && viewport.selectors[selector] ? viewport.selectors[selector].count : 0) },
10226
+ message: failed.length ? "Selector " + selector + " count did not equal " + expectedCount + " in " + failed.length + " viewport(s)." : undefined,
10227
+ });
10228
+ continue;
10229
+ }
10148
10230
  if (check.type === "selector_text_order") {
10149
10231
  const selector = check.selector || "";
10150
10232
  const expectedTexts = check.expected_texts || [];
@@ -10320,11 +10402,11 @@ function assessProfile(profile, evidence) {
10320
10402
  }
10321
10403
  if (check.type === "no_fatal_console_errors") {
10322
10404
  const fatalConsoleEvents = ((evidence.console && evidence.console.events) || []).filter((event) => event && (event.type === "error" || event.type === "assert"));
10323
- const allowedConsoleEvents = fatalConsoleEvents.filter((event) => matchesAllowedMessage(event.text, check.allowed_console_texts, check.allowed_console_patterns));
10324
- const unallowedConsoleEvents = fatalConsoleEvents.filter((event) => !matchesAllowedMessage(event.text, check.allowed_console_texts, check.allowed_console_patterns));
10405
+ const allowedConsoleEvents = fatalConsoleEvents.filter((event) => matchesAllowedMessage(event, check.allowed_console_texts, check.allowed_console_patterns));
10406
+ const unallowedConsoleEvents = fatalConsoleEvents.filter((event) => !matchesAllowedMessage(event, check.allowed_console_texts, check.allowed_console_patterns));
10325
10407
  const pageErrors = evidence.page_errors || [];
10326
- const allowedPageErrors = pageErrors.filter((error) => matchesAllowedMessage(error && error.message, check.allowed_page_error_texts, check.allowed_page_error_patterns));
10327
- const unallowedPageErrors = pageErrors.filter((error) => !matchesAllowedMessage(error && error.message, check.allowed_page_error_texts, check.allowed_page_error_patterns));
10408
+ const allowedPageErrors = pageErrors.filter((error) => matchesAllowedMessage(error, check.allowed_page_error_texts, check.allowed_page_error_patterns));
10409
+ const unallowedPageErrors = pageErrors.filter((error) => !matchesAllowedMessage(error, check.allowed_page_error_texts, check.allowed_page_error_patterns));
10328
10410
  const fatalCount = unallowedConsoleEvents.length + unallowedPageErrors.length;
10329
10411
  checks.push({
10330
10412
  type: check.type,
@@ -11161,7 +11243,16 @@ async function captureViewport(viewport) {
11161
11243
  const text_sequences = {};
11162
11244
  const text_matches = {};
11163
11245
  for (const check of profile.checks || []) {
11164
- if ((check.type === "selector_visible" || check.type === "selector_count_at_least") && check.selector) {
11246
+ if (
11247
+ (
11248
+ check.type === "selector_visible"
11249
+ || check.type === "selector_absent"
11250
+ || check.type === "selector_count_at_least"
11251
+ || check.type === "selector_count_equals"
11252
+ || check.type === "selector_count_equal"
11253
+ || check.type === "selector_count_eq"
11254
+ ) && check.selector
11255
+ ) {
11165
11256
  selectors[check.selector] = await selectorStats(check.selector);
11166
11257
  }
11167
11258
  if (check.type === "selector_text_order" && check.selector) {
package/dist/index.js CHANGED
@@ -58,7 +58,7 @@ import {
58
58
  resolveRiddleProofProfileTimeoutSec,
59
59
  slugifyRiddleProofProfileName,
60
60
  summarizeRiddleProofProfileResult
61
- } from "./chunk-6MVVU54I.js";
61
+ } from "./chunk-ZB5DSQF2.js";
62
62
  import {
63
63
  DEFAULT_RIDDLE_API_BASE_URL,
64
64
  DEFAULT_RIDDLE_API_KEY_FILE,
package/dist/profile.cjs CHANGED
@@ -56,7 +56,11 @@ var RIDDLE_PROOF_PROFILE_STATUSES = [
56
56
  var RIDDLE_PROOF_PROFILE_CHECK_TYPES = [
57
57
  "route_loaded",
58
58
  "selector_visible",
59
+ "selector_absent",
59
60
  "selector_count_at_least",
61
+ "selector_count_equals",
62
+ "selector_count_equal",
63
+ "selector_count_eq",
60
64
  "selector_text_order",
61
65
  "frame_text_visible",
62
66
  "frame_no_horizontal_overflow",
@@ -396,7 +400,7 @@ function normalizeCheck(input, index) {
396
400
  if (!isSupportedCheckType(type)) {
397
401
  throw new Error(`checks[${index}].type ${type} is not supported. Supported checks: ${RIDDLE_PROOF_PROFILE_CHECK_TYPES.join(", ")}`);
398
402
  }
399
- if ((type === "selector_visible" || type === "selector_count_at_least") && !stringValue(input.selector)) {
403
+ if ((type === "selector_visible" || type === "selector_absent" || type === "selector_count_at_least" || type === "selector_count_equals" || type === "selector_count_equal" || type === "selector_count_eq") && !stringValue(input.selector)) {
400
404
  throw new Error(`checks[${index}] ${type} requires selector.`);
401
405
  }
402
406
  if ((type === "frame_text_visible" || type === "frame_no_horizontal_overflow") && !stringValue(input.selector)) {
@@ -411,6 +415,10 @@ function normalizeCheck(input, index) {
411
415
  if (type === "selector_count_at_least" && numberValue(input.min_count) === void 0) {
412
416
  throw new Error(`checks[${index}] selector_count_at_least requires min_count.`);
413
417
  }
418
+ const expectedCount = numberValue(input.expected_count) ?? numberValue(input.expectedCount) ?? numberValue(input.count);
419
+ if ((type === "selector_count_equals" || type === "selector_count_equal" || type === "selector_count_eq") && expectedCount === void 0) {
420
+ throw new Error(`checks[${index}] ${type} requires expected_count.`);
421
+ }
414
422
  const expectedTexts = normalizeExpectedTexts(input.expected_texts ?? input.expectedTexts, index);
415
423
  if (type === "selector_text_order") {
416
424
  if (!stringValue(input.selector)) throw new Error(`checks[${index}] selector_text_order requires selector.`);
@@ -442,6 +450,7 @@ function normalizeCheck(input, index) {
442
450
  allowed_page_error_texts: normalizeStringList(input.allowed_page_error_texts ?? input.allowedPageErrorTexts ?? input.allow_page_error_texts ?? input.allowPageErrorTexts, `checks[${index}] allowed_page_error_texts`),
443
451
  allowed_page_error_patterns: normalizeStringList(input.allowed_page_error_patterns ?? input.allowedPageErrorPatterns ?? input.allow_page_error_patterns ?? input.allowPageErrorPatterns, `checks[${index}] allowed_page_error_patterns`),
444
452
  min_count: numberValue(input.min_count),
453
+ expected_count: expectedCount,
445
454
  max_overflow_px: numberValue(input.max_overflow_px),
446
455
  timeout_ms: numberValue(input.timeout_ms) ?? numberValue(input.timeoutMs),
447
456
  run_direct_routes: input.run_direct_routes === false || input.runDirectRoutes === false ? false : true,
@@ -604,8 +613,17 @@ function matchText(sample, check) {
604
613
  }
605
614
  return sample.includes(check.text || "");
606
615
  }
607
- function matchesAllowedMessage(message, texts, patterns) {
608
- const sample = String(message || "");
616
+ function allowedMessageSample(input) {
617
+ if (!isRecord(input)) return String(input || "");
618
+ const parts = [
619
+ input.text,
620
+ input.message,
621
+ isRecord(input.location) ? input.location.url : void 0
622
+ ];
623
+ return parts.map((part) => String(part || "")).filter(Boolean).join(" ");
624
+ }
625
+ function matchesAllowedMessage(input, texts, patterns) {
626
+ const sample = allowedMessageSample(input);
609
627
  if (texts?.some((text) => sample.includes(text))) return true;
610
628
  for (const pattern of patterns || []) {
611
629
  try {
@@ -709,6 +727,20 @@ function assessCheckFromEvidence(check, evidence) {
709
727
  message: failed.length ? `Selector ${key} was not visible in ${failed.length} viewport(s).` : void 0
710
728
  };
711
729
  }
730
+ if (check.type === "selector_absent") {
731
+ const key = selectorKey(check);
732
+ const failed = viewports.filter((viewport) => (viewport.selectors?.[key]?.count || 0) > 0);
733
+ return {
734
+ type: check.type,
735
+ label: checkLabel(check),
736
+ status: failed.length ? "failed" : "passed",
737
+ evidence: {
738
+ selector: key,
739
+ counts: viewports.map((viewport) => viewport.selectors?.[key]?.count || 0)
740
+ },
741
+ message: failed.length ? `Selector ${key} was present in ${failed.length} viewport(s).` : void 0
742
+ };
743
+ }
712
744
  if (check.type === "selector_count_at_least") {
713
745
  const key = selectorKey(check);
714
746
  const minCount = check.min_count ?? 1;
@@ -725,6 +757,22 @@ function assessCheckFromEvidence(check, evidence) {
725
757
  message: failed.length ? `Selector ${key} count was below ${minCount} in ${failed.length} viewport(s).` : void 0
726
758
  };
727
759
  }
760
+ if (check.type === "selector_count_equals" || check.type === "selector_count_equal" || check.type === "selector_count_eq") {
761
+ const key = selectorKey(check);
762
+ const expectedCount = check.expected_count ?? 0;
763
+ const failed = viewports.filter((viewport) => (viewport.selectors?.[key]?.count || 0) !== expectedCount);
764
+ return {
765
+ type: check.type,
766
+ label: checkLabel(check),
767
+ status: failed.length ? "failed" : "passed",
768
+ evidence: {
769
+ selector: key,
770
+ expected_count: expectedCount,
771
+ counts: viewports.map((viewport) => viewport.selectors?.[key]?.count || 0)
772
+ },
773
+ message: failed.length ? `Selector ${key} count did not equal ${expectedCount} in ${failed.length} viewport(s).` : void 0
774
+ };
775
+ }
728
776
  if (check.type === "selector_text_order") {
729
777
  const key = selectorKey(check);
730
778
  const expectedTexts = check.expected_texts || [];
@@ -903,11 +951,11 @@ function assessCheckFromEvidence(check, evidence) {
903
951
  }
904
952
  if (check.type === "no_fatal_console_errors") {
905
953
  const fatalConsoleEvents = (evidence.console?.events || []).filter((event) => event.type === "error" || event.type === "assert");
906
- const allowedConsoleEvents = fatalConsoleEvents.filter((event) => matchesAllowedMessage(event.text, check.allowed_console_texts, check.allowed_console_patterns));
907
- const unallowedConsoleEvents = fatalConsoleEvents.filter((event) => !matchesAllowedMessage(event.text, check.allowed_console_texts, check.allowed_console_patterns));
954
+ const allowedConsoleEvents = fatalConsoleEvents.filter((event) => matchesAllowedMessage(event, check.allowed_console_texts, check.allowed_console_patterns));
955
+ const unallowedConsoleEvents = fatalConsoleEvents.filter((event) => !matchesAllowedMessage(event, check.allowed_console_texts, check.allowed_console_patterns));
908
956
  const pageErrors = evidence.page_errors || [];
909
- const allowedPageErrors = pageErrors.filter((error) => matchesAllowedMessage(error.message, check.allowed_page_error_texts, check.allowed_page_error_patterns));
910
- const unallowedPageErrors = pageErrors.filter((error) => !matchesAllowedMessage(error.message, check.allowed_page_error_texts, check.allowed_page_error_patterns));
957
+ const allowedPageErrors = pageErrors.filter((error) => matchesAllowedMessage(error, check.allowed_page_error_texts, check.allowed_page_error_patterns));
958
+ const unallowedPageErrors = pageErrors.filter((error) => !matchesAllowedMessage(error, check.allowed_page_error_texts, check.allowed_page_error_patterns));
911
959
  const fatalCount = unallowedConsoleEvents.length + unallowedPageErrors.length;
912
960
  return {
913
961
  type: check.type,
@@ -1192,8 +1240,17 @@ function textMatches(sample, check) {
1192
1240
  }
1193
1241
  return String(sample || "").includes(check.text || "");
1194
1242
  }
1195
- function matchesAllowedMessage(message, texts, patterns) {
1196
- const sample = String(message || "");
1243
+ function allowedMessageSample(input) {
1244
+ if (!input || typeof input !== "object" || Array.isArray(input)) return String(input || "");
1245
+ const parts = [
1246
+ input.text,
1247
+ input.message,
1248
+ input.location && typeof input.location === "object" ? input.location.url : undefined,
1249
+ ];
1250
+ return parts.map((part) => String(part || "")).filter(Boolean).join(" ");
1251
+ }
1252
+ function matchesAllowedMessage(input, texts, patterns) {
1253
+ const sample = allowedMessageSample(input);
1197
1254
  if ((texts || []).some((text) => sample.includes(text))) return true;
1198
1255
  for (const pattern of patterns || []) {
1199
1256
  try {
@@ -1461,6 +1518,18 @@ function assessProfile(profile, evidence) {
1461
1518
  });
1462
1519
  continue;
1463
1520
  }
1521
+ if (check.type === "selector_absent") {
1522
+ const selector = check.selector || "";
1523
+ const failed = viewports.filter((viewport) => viewport.selectors && viewport.selectors[selector] && viewport.selectors[selector].count > 0);
1524
+ checks.push({
1525
+ type: check.type,
1526
+ label: check.label || check.type,
1527
+ status: failed.length ? "failed" : "passed",
1528
+ evidence: { selector, counts: viewports.map((viewport) => viewport.selectors && viewport.selectors[selector] ? viewport.selectors[selector].count : 0) },
1529
+ message: failed.length ? "Selector " + selector + " was present in " + failed.length + " viewport(s)." : undefined,
1530
+ });
1531
+ continue;
1532
+ }
1464
1533
  if (check.type === "selector_count_at_least") {
1465
1534
  const selector = check.selector || "";
1466
1535
  const minCount = check.min_count == null ? 1 : check.min_count;
@@ -1474,6 +1543,19 @@ function assessProfile(profile, evidence) {
1474
1543
  });
1475
1544
  continue;
1476
1545
  }
1546
+ if (check.type === "selector_count_equals" || check.type === "selector_count_equal" || check.type === "selector_count_eq") {
1547
+ const selector = check.selector || "";
1548
+ const expectedCount = check.expected_count == null ? 0 : check.expected_count;
1549
+ const failed = viewports.filter((viewport) => (viewport.selectors && viewport.selectors[selector] ? viewport.selectors[selector].count : 0) !== expectedCount);
1550
+ checks.push({
1551
+ type: check.type,
1552
+ label: check.label || check.type,
1553
+ status: failed.length ? "failed" : "passed",
1554
+ evidence: { selector, expected_count: expectedCount, counts: viewports.map((viewport) => viewport.selectors && viewport.selectors[selector] ? viewport.selectors[selector].count : 0) },
1555
+ message: failed.length ? "Selector " + selector + " count did not equal " + expectedCount + " in " + failed.length + " viewport(s)." : undefined,
1556
+ });
1557
+ continue;
1558
+ }
1477
1559
  if (check.type === "selector_text_order") {
1478
1560
  const selector = check.selector || "";
1479
1561
  const expectedTexts = check.expected_texts || [];
@@ -1649,11 +1731,11 @@ function assessProfile(profile, evidence) {
1649
1731
  }
1650
1732
  if (check.type === "no_fatal_console_errors") {
1651
1733
  const fatalConsoleEvents = ((evidence.console && evidence.console.events) || []).filter((event) => event && (event.type === "error" || event.type === "assert"));
1652
- const allowedConsoleEvents = fatalConsoleEvents.filter((event) => matchesAllowedMessage(event.text, check.allowed_console_texts, check.allowed_console_patterns));
1653
- const unallowedConsoleEvents = fatalConsoleEvents.filter((event) => !matchesAllowedMessage(event.text, check.allowed_console_texts, check.allowed_console_patterns));
1734
+ const allowedConsoleEvents = fatalConsoleEvents.filter((event) => matchesAllowedMessage(event, check.allowed_console_texts, check.allowed_console_patterns));
1735
+ const unallowedConsoleEvents = fatalConsoleEvents.filter((event) => !matchesAllowedMessage(event, check.allowed_console_texts, check.allowed_console_patterns));
1654
1736
  const pageErrors = evidence.page_errors || [];
1655
- const allowedPageErrors = pageErrors.filter((error) => matchesAllowedMessage(error && error.message, check.allowed_page_error_texts, check.allowed_page_error_patterns));
1656
- const unallowedPageErrors = pageErrors.filter((error) => !matchesAllowedMessage(error && error.message, check.allowed_page_error_texts, check.allowed_page_error_patterns));
1737
+ const allowedPageErrors = pageErrors.filter((error) => matchesAllowedMessage(error, check.allowed_page_error_texts, check.allowed_page_error_patterns));
1738
+ const unallowedPageErrors = pageErrors.filter((error) => !matchesAllowedMessage(error, check.allowed_page_error_texts, check.allowed_page_error_patterns));
1657
1739
  const fatalCount = unallowedConsoleEvents.length + unallowedPageErrors.length;
1658
1740
  checks.push({
1659
1741
  type: check.type,
@@ -2490,7 +2572,16 @@ async function captureViewport(viewport) {
2490
2572
  const text_sequences = {};
2491
2573
  const text_matches = {};
2492
2574
  for (const check of profile.checks || []) {
2493
- if ((check.type === "selector_visible" || check.type === "selector_count_at_least") && check.selector) {
2575
+ if (
2576
+ (
2577
+ check.type === "selector_visible"
2578
+ || check.type === "selector_absent"
2579
+ || check.type === "selector_count_at_least"
2580
+ || check.type === "selector_count_equals"
2581
+ || check.type === "selector_count_equal"
2582
+ || check.type === "selector_count_eq"
2583
+ ) && check.selector
2584
+ ) {
2494
2585
  selectors[check.selector] = await selectorStats(check.selector);
2495
2586
  }
2496
2587
  if (check.type === "selector_text_order" && check.selector) {
@@ -4,7 +4,7 @@ declare const RIDDLE_PROOF_PROFILE_VERSION: "riddle-proof.profile.v1";
4
4
  declare const RIDDLE_PROOF_PROFILE_EVIDENCE_VERSION: "riddle-proof.profile-evidence.v1";
5
5
  declare const RIDDLE_PROOF_PROFILE_RESULT_VERSION: "riddle-proof.profile-result.v1";
6
6
  declare const RIDDLE_PROOF_PROFILE_STATUSES: readonly ["passed", "product_regression", "proof_insufficient", "environment_blocked", "configuration_error", "needs_human_review"];
7
- declare const RIDDLE_PROOF_PROFILE_CHECK_TYPES: readonly ["route_loaded", "selector_visible", "selector_count_at_least", "selector_text_order", "frame_text_visible", "frame_no_horizontal_overflow", "text_visible", "text_absent", "route_inventory", "no_horizontal_overflow", "no_mobile_horizontal_overflow", "no_fatal_console_errors"];
7
+ declare const RIDDLE_PROOF_PROFILE_CHECK_TYPES: readonly ["route_loaded", "selector_visible", "selector_absent", "selector_count_at_least", "selector_count_equals", "selector_count_equal", "selector_count_eq", "selector_text_order", "frame_text_visible", "frame_no_horizontal_overflow", "text_visible", "text_absent", "route_inventory", "no_horizontal_overflow", "no_mobile_horizontal_overflow", "no_fatal_console_errors"];
8
8
  declare const RIDDLE_PROOF_PROFILE_SETUP_ACTION_TYPES: readonly ["click", "fill", "set_input_value", "local_storage", "session_storage", "clear_storage", "wait", "wait_for_selector", "wait_for_text"];
9
9
  type RiddleProofProfileStatus = typeof RIDDLE_PROOF_PROFILE_STATUSES[number];
10
10
  type RiddleProofProfileCheckType = typeof RIDDLE_PROOF_PROFILE_CHECK_TYPES[number];
@@ -88,6 +88,7 @@ interface RiddleProofProfileCheck {
88
88
  allowed_page_error_texts?: string[];
89
89
  allowed_page_error_patterns?: string[];
90
90
  min_count?: number;
91
+ expected_count?: number;
91
92
  max_overflow_px?: number;
92
93
  timeout_ms?: number;
93
94
  run_direct_routes?: boolean;
package/dist/profile.d.ts CHANGED
@@ -4,7 +4,7 @@ declare const RIDDLE_PROOF_PROFILE_VERSION: "riddle-proof.profile.v1";
4
4
  declare const RIDDLE_PROOF_PROFILE_EVIDENCE_VERSION: "riddle-proof.profile-evidence.v1";
5
5
  declare const RIDDLE_PROOF_PROFILE_RESULT_VERSION: "riddle-proof.profile-result.v1";
6
6
  declare const RIDDLE_PROOF_PROFILE_STATUSES: readonly ["passed", "product_regression", "proof_insufficient", "environment_blocked", "configuration_error", "needs_human_review"];
7
- declare const RIDDLE_PROOF_PROFILE_CHECK_TYPES: readonly ["route_loaded", "selector_visible", "selector_count_at_least", "selector_text_order", "frame_text_visible", "frame_no_horizontal_overflow", "text_visible", "text_absent", "route_inventory", "no_horizontal_overflow", "no_mobile_horizontal_overflow", "no_fatal_console_errors"];
7
+ declare const RIDDLE_PROOF_PROFILE_CHECK_TYPES: readonly ["route_loaded", "selector_visible", "selector_absent", "selector_count_at_least", "selector_count_equals", "selector_count_equal", "selector_count_eq", "selector_text_order", "frame_text_visible", "frame_no_horizontal_overflow", "text_visible", "text_absent", "route_inventory", "no_horizontal_overflow", "no_mobile_horizontal_overflow", "no_fatal_console_errors"];
8
8
  declare const RIDDLE_PROOF_PROFILE_SETUP_ACTION_TYPES: readonly ["click", "fill", "set_input_value", "local_storage", "session_storage", "clear_storage", "wait", "wait_for_selector", "wait_for_text"];
9
9
  type RiddleProofProfileStatus = typeof RIDDLE_PROOF_PROFILE_STATUSES[number];
10
10
  type RiddleProofProfileCheckType = typeof RIDDLE_PROOF_PROFILE_CHECK_TYPES[number];
@@ -88,6 +88,7 @@ interface RiddleProofProfileCheck {
88
88
  allowed_page_error_texts?: string[];
89
89
  allowed_page_error_patterns?: string[];
90
90
  min_count?: number;
91
+ expected_count?: number;
91
92
  max_overflow_px?: number;
92
93
  timeout_ms?: number;
93
94
  run_direct_routes?: boolean;
package/dist/profile.js CHANGED
@@ -19,7 +19,7 @@ import {
19
19
  resolveRiddleProofProfileTimeoutSec,
20
20
  slugifyRiddleProofProfileName,
21
21
  summarizeRiddleProofProfileResult
22
- } from "./chunk-6MVVU54I.js";
22
+ } from "./chunk-ZB5DSQF2.js";
23
23
  export {
24
24
  RIDDLE_PROOF_PROFILE_CHECK_TYPES,
25
25
  RIDDLE_PROOF_PROFILE_EVIDENCE_VERSION,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@riddledc/riddle-proof",
3
- "version": "0.7.41",
3
+ "version": "0.7.43",
4
4
  "description": "Reusable Riddle Proof contracts and helpers for evidence-backed agent changes.",
5
5
  "license": "MIT",
6
6
  "author": "RiddleDC",