@riddledc/riddle-proof 0.7.94 → 0.7.96

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
@@ -479,6 +479,8 @@ artifact links should fail the profile:
479
479
  "expected_count": 88,
480
480
  "same_origin_only": true,
481
481
  "require_nonzero_bytes": true,
482
+ "min_bytes": 32,
483
+ "allowed_content_types": ["image/*", "application/json"],
482
484
  "max_links": 150
483
485
  }
484
486
  ```
@@ -486,9 +488,12 @@ artifact links should fail the profile:
486
488
  The check defaults to `a[href]`, deduplicates URLs, probes up to 100 selected
487
489
  URLs, and treats HTTP `2xx` / `3xx` responses as healthy. Use
488
490
  `allowed_statuses` or `expected_status` when a narrower status contract is
489
- intentional, `min_count` for lower-bound audits, and `max_links` when the
490
- selected set is intentionally larger than 100. `artifact_link_status` is an
491
- alias with the same behavior for profiles that want artifact-specific wording.
491
+ intentional, `min_count` for lower-bound audits, `min_bytes` when a one-byte
492
+ range response is too weak, `allowed_content_types` for MIME checks, and
493
+ `max_links` when the selected set is intentionally larger than 100.
494
+ `allowed_content_types` accepts exact types and family wildcards such as
495
+ `image/*`. `artifact_link_status` is an alias with the same behavior for
496
+ profiles that want artifact-specific wording.
492
497
 
493
498
  Use the `route_inventory` check for source-page route coverage audits where a
494
499
  navigation surface must expose a known set of routes and each route must load
@@ -785,6 +785,12 @@ function normalizeCheck(input, index) {
785
785
  `checks[${index}] allowed_statuses`
786
786
  ) : void 0;
787
787
  const maxLinks = isLinkStatusCheck ? normalizePositiveInteger(input.max_links ?? input.maxLinks ?? input.limit, `checks[${index}] max_links`, 500) : void 0;
788
+ const minBytes = isLinkStatusCheck ? normalizePositiveInteger(input.min_bytes ?? input.minBytes ?? input.min_response_bytes ?? input.minResponseBytes, `checks[${index}] min_bytes`) : void 0;
789
+ const expectedContentType = isLinkStatusCheck ? stringValue(input.expected_content_type) || stringValue(input.expectedContentType) || stringValue(input.content_type) || stringValue(input.contentType) : void 0;
790
+ const allowedContentTypes = isLinkStatusCheck ? normalizeStringList(
791
+ input.allowed_content_types ?? input.allowedContentTypes ?? input.expected_content_types ?? input.expectedContentTypes,
792
+ `checks[${index}] allowed_content_types`
793
+ ) ?? (expectedContentType ? [expectedContentType] : void 0) : void 0;
788
794
  if (isLinkStatusCheck) {
789
795
  if (minCount !== void 0 && (!Number.isInteger(minCount) || minCount < 0)) {
790
796
  throw new Error(`checks[${index}] ${type} min_count must be a non-negative integer.`);
@@ -826,6 +832,8 @@ function normalizeCheck(input, index) {
826
832
  same_origin_only: isLinkStatusCheck ? input.same_origin_only === true || input.sameOriginOnly === true : void 0,
827
833
  dedupe: isLinkStatusCheck ? input.dedupe === false ? false : true : void 0,
828
834
  require_nonzero_bytes: isLinkStatusCheck ? input.require_nonzero_bytes === true || input.requireNonzeroBytes === true : void 0,
835
+ min_bytes: minBytes,
836
+ allowed_content_types: allowedContentTypes,
829
837
  allow_get_fallback: isLinkStatusCheck ? input.allow_get_fallback === false || input.allowGetFallback === false ? false : true : void 0,
830
838
  max_overflow_px: numberValue(input.max_overflow_px),
831
839
  timeout_ms: numberValue(input.timeout_ms) ?? numberValue(input.timeoutMs),
@@ -932,15 +940,40 @@ function linkStatusIsAllowed(status, check) {
932
940
  const allowed = linkStatusAllowedStatuses(check);
933
941
  return allowed?.length ? allowed.includes(status) : status >= 200 && status < 400;
934
942
  }
943
+ function linkStatusObservedBytes(result) {
944
+ const bytes = numberValue(result.bytes);
945
+ const contentLength = numberValue(result.content_length);
946
+ return Math.max(bytes ?? 0, contentLength ?? 0) || void 0;
947
+ }
948
+ function normalizeLinkStatusContentType(value) {
949
+ const normalized = value?.split(";")[0]?.trim().toLowerCase();
950
+ return normalized || void 0;
951
+ }
952
+ function linkStatusContentTypeOk(result, check) {
953
+ const expected = check.allowed_content_types;
954
+ if (!expected?.length) return true;
955
+ const actual = normalizeLinkStatusContentType(stringValue(result.content_type));
956
+ if (!actual) return false;
957
+ return expected.some((contentType) => {
958
+ const normalized = normalizeLinkStatusContentType(contentType);
959
+ if (!normalized) return false;
960
+ if (normalized.endsWith("/*")) return actual.startsWith(normalized.slice(0, -1));
961
+ return actual === normalized;
962
+ });
963
+ }
935
964
  function linkStatusResultOk(result, check) {
936
965
  const status = numberValue(result.status);
937
966
  if (!linkStatusIsAllowed(status, check)) return false;
938
967
  if (stringValue(result.error)) return false;
939
968
  if (result.ok === false) return false;
969
+ if (!linkStatusContentTypeOk(result, check)) return false;
940
970
  if (check.require_nonzero_bytes) {
941
- const bytes = numberValue(result.bytes);
942
- const contentLength = numberValue(result.content_length);
943
- return bytes !== void 0 && bytes > 0 || contentLength !== void 0 && contentLength > 0;
971
+ const observedBytes = linkStatusObservedBytes(result);
972
+ if (observedBytes === void 0 || observedBytes <= 0) return false;
973
+ }
974
+ if (check.min_bytes !== void 0) {
975
+ const observedBytes = linkStatusObservedBytes(result);
976
+ if (observedBytes === void 0 || observedBytes < check.min_bytes) return false;
944
977
  }
945
978
  return true;
946
979
  }
@@ -962,6 +995,10 @@ function summarizeLinkStatusEvidence(viewport, check) {
962
995
  }
963
996
  const results = Array.isArray(linkEvidence.results) ? linkEvidence.results.filter(isRecord) : [];
964
997
  const totalCount = numberValue(linkEvidence.total_count) ?? results.length;
998
+ const resultCount = numberValue(linkEvidence.result_count) ?? totalCount;
999
+ const storedResultCount = numberValue(linkEvidence.stored_result_count) ?? results.length;
1000
+ const omittedResultCount = numberValue(linkEvidence.omitted_result_count) ?? Math.max(0, resultCount - storedResultCount);
1001
+ const omittedSuccessCount = numberValue(linkEvidence.omitted_success_count) ?? 0;
965
1002
  const okCount = results.filter((result) => linkStatusResultOk(result, check)).length;
966
1003
  const failures = results.filter((result) => !linkStatusResultOk(result, check)).map((result) => ({
967
1004
  code: "link_status_failed",
@@ -969,7 +1006,10 @@ function summarizeLinkStatusEvidence(viewport, check) {
969
1006
  status: numberValue(result.status) ?? null,
970
1007
  method: stringValue(result.method) ?? null,
971
1008
  error: stringValue(result.error) ?? null,
972
- bytes: numberValue(result.bytes) ?? numberValue(result.content_length) ?? null
1009
+ content_type: stringValue(result.content_type) ?? null,
1010
+ bytes: linkStatusObservedBytes(result) ?? null,
1011
+ min_bytes: check.min_bytes ?? null,
1012
+ allowed_content_types: check.allowed_content_types ?? null
973
1013
  }));
974
1014
  if (stringValue(linkEvidence.error)) {
975
1015
  failures.push({ code: "link_status_capture_failed", error: stringValue(linkEvidence.error) ?? "" });
@@ -1006,6 +1046,13 @@ function summarizeLinkStatusEvidence(viewport, check) {
1006
1046
  failed_count: failures.length,
1007
1047
  truncated: linkEvidence.truncated === true,
1008
1048
  max_links: numberValue(linkEvidence.max_links) ?? check.max_links ?? 100,
1049
+ result_count: resultCount,
1050
+ stored_result_count: storedResultCount,
1051
+ omitted_result_count: omittedResultCount,
1052
+ omitted_success_count: omittedSuccessCount,
1053
+ results_compacted: linkEvidence.results_compacted === true || omittedResultCount > 0,
1054
+ min_bytes: check.min_bytes ?? null,
1055
+ allowed_content_types: check.allowed_content_types ?? null,
1009
1056
  status_counts: statusCounts,
1010
1057
  failures: failures.slice(0, 20)
1011
1058
  };
@@ -1534,6 +1581,8 @@ function assessCheckFromEvidence(check, evidence) {
1534
1581
  min_count: check.min_count ?? null,
1535
1582
  allowed_statuses: linkStatusAllowedStatuses(check) || ["2xx", "3xx"],
1536
1583
  require_nonzero_bytes: check.require_nonzero_bytes === true,
1584
+ min_bytes: check.min_bytes ?? null,
1585
+ allowed_content_types: check.allowed_content_types ?? null,
1537
1586
  viewports: summaries.map((summary) => toJsonValue(summary)),
1538
1587
  failures: failed.flatMap((summary) => Array.isArray(summary.failures) ? summary.failures.map((failure) => toJsonValue({ viewport: stringValue(summary.viewport) ?? null, failure })) : [])
1539
1588
  },
@@ -2137,15 +2186,41 @@ function linkStatusIsAllowed(status, check) {
2137
2186
  const allowed = linkStatusAllowedStatuses(check);
2138
2187
  return Array.isArray(allowed) && allowed.length ? allowed.includes(status) : status >= 200 && status < 400;
2139
2188
  }
2189
+ function linkStatusObservedBytes(result) {
2190
+ const bytes = typeof result.bytes === "number" && Number.isFinite(result.bytes) ? result.bytes : undefined;
2191
+ const contentLength = typeof result.content_length === "number" && Number.isFinite(result.content_length) ? result.content_length : undefined;
2192
+ const observed = Math.max(bytes || 0, contentLength || 0);
2193
+ return observed > 0 ? observed : undefined;
2194
+ }
2195
+ function normalizeLinkStatusContentType(value) {
2196
+ if (typeof value !== "string") return undefined;
2197
+ const normalized = (value.split(";")[0] || "").trim().toLowerCase();
2198
+ return normalized || undefined;
2199
+ }
2200
+ function linkStatusContentTypeOk(result, check) {
2201
+ if (!Array.isArray(check.allowed_content_types) || !check.allowed_content_types.length) return true;
2202
+ const actual = normalizeLinkStatusContentType(result.content_type);
2203
+ if (!actual) return false;
2204
+ return check.allowed_content_types.some((contentType) => {
2205
+ const normalized = normalizeLinkStatusContentType(contentType);
2206
+ if (!normalized) return false;
2207
+ if (normalized.endsWith("/*")) return actual.startsWith(normalized.slice(0, -1));
2208
+ return actual === normalized;
2209
+ });
2210
+ }
2140
2211
  function linkStatusResultOk(result, check) {
2141
2212
  if (!result || typeof result !== "object" || Array.isArray(result)) return false;
2142
2213
  if (!linkStatusIsAllowed(result.status, check)) return false;
2143
2214
  if (typeof result.error === "string" && result.error.trim()) return false;
2144
2215
  if (result.ok === false) return false;
2216
+ if (!linkStatusContentTypeOk(result, check)) return false;
2145
2217
  if (check.require_nonzero_bytes === true) {
2146
- const bytes = typeof result.bytes === "number" && Number.isFinite(result.bytes) ? result.bytes : undefined;
2147
- const contentLength = typeof result.content_length === "number" && Number.isFinite(result.content_length) ? result.content_length : undefined;
2148
- return (bytes !== undefined && bytes > 0) || (contentLength !== undefined && contentLength > 0);
2218
+ const observedBytes = linkStatusObservedBytes(result);
2219
+ if (observedBytes === undefined || observedBytes <= 0) return false;
2220
+ }
2221
+ if (typeof check.min_bytes === "number" && Number.isFinite(check.min_bytes)) {
2222
+ const observedBytes = linkStatusObservedBytes(result);
2223
+ if (observedBytes === undefined || observedBytes < check.min_bytes) return false;
2149
2224
  }
2150
2225
  return true;
2151
2226
  }
@@ -2164,6 +2239,10 @@ function summarizeLinkStatusEvidence(viewport, check) {
2164
2239
  }
2165
2240
  const results = Array.isArray(linkEvidence.results) ? linkEvidence.results.filter((result) => result && typeof result === "object" && !Array.isArray(result)) : [];
2166
2241
  const totalCount = typeof linkEvidence.total_count === "number" && Number.isFinite(linkEvidence.total_count) ? linkEvidence.total_count : results.length;
2242
+ const resultCount = typeof linkEvidence.result_count === "number" && Number.isFinite(linkEvidence.result_count) ? linkEvidence.result_count : totalCount;
2243
+ const storedResultCount = typeof linkEvidence.stored_result_count === "number" && Number.isFinite(linkEvidence.stored_result_count) ? linkEvidence.stored_result_count : results.length;
2244
+ const omittedResultCount = typeof linkEvidence.omitted_result_count === "number" && Number.isFinite(linkEvidence.omitted_result_count) ? linkEvidence.omitted_result_count : Math.max(0, resultCount - storedResultCount);
2245
+ const omittedSuccessCount = typeof linkEvidence.omitted_success_count === "number" && Number.isFinite(linkEvidence.omitted_success_count) ? linkEvidence.omitted_success_count : 0;
2167
2246
  const okCount = results.filter((result) => linkStatusResultOk(result, check)).length;
2168
2247
  const failures = results
2169
2248
  .filter((result) => !linkStatusResultOk(result, check))
@@ -2173,11 +2252,10 @@ function summarizeLinkStatusEvidence(viewport, check) {
2173
2252
  status: typeof result.status === "number" && Number.isFinite(result.status) ? result.status : null,
2174
2253
  method: typeof result.method === "string" ? result.method : null,
2175
2254
  error: typeof result.error === "string" ? result.error : null,
2176
- bytes: typeof result.bytes === "number" && Number.isFinite(result.bytes)
2177
- ? result.bytes
2178
- : typeof result.content_length === "number" && Number.isFinite(result.content_length)
2179
- ? result.content_length
2180
- : null,
2255
+ content_type: typeof result.content_type === "string" ? result.content_type : null,
2256
+ bytes: linkStatusObservedBytes(result) ?? null,
2257
+ min_bytes: typeof check.min_bytes === "number" && Number.isFinite(check.min_bytes) ? check.min_bytes : null,
2258
+ allowed_content_types: Array.isArray(check.allowed_content_types) ? check.allowed_content_types : null,
2181
2259
  }));
2182
2260
  if (typeof linkEvidence.error === "string" && linkEvidence.error.trim()) {
2183
2261
  failures.push({ code: "link_status_capture_failed", error: linkEvidence.error });
@@ -2212,6 +2290,13 @@ function summarizeLinkStatusEvidence(viewport, check) {
2212
2290
  failed_count: failures.length,
2213
2291
  truncated: linkEvidence.truncated === true,
2214
2292
  max_links: typeof linkEvidence.max_links === "number" ? linkEvidence.max_links : check.max_links || 100,
2293
+ result_count: resultCount,
2294
+ stored_result_count: storedResultCount,
2295
+ omitted_result_count: omittedResultCount,
2296
+ omitted_success_count: omittedSuccessCount,
2297
+ results_compacted: linkEvidence.results_compacted === true || omittedResultCount > 0,
2298
+ min_bytes: typeof check.min_bytes === "number" && Number.isFinite(check.min_bytes) ? check.min_bytes : null,
2299
+ allowed_content_types: Array.isArray(check.allowed_content_types) ? check.allowed_content_types : null,
2215
2300
  status_counts: linkEvidence.status_counts && typeof linkEvidence.status_counts === "object" && !Array.isArray(linkEvidence.status_counts) ? linkEvidence.status_counts : {},
2216
2301
  failures: failures.slice(0, 20),
2217
2302
  };
@@ -2908,6 +2993,8 @@ function assessProfile(profile, evidence) {
2908
2993
  min_count: check.min_count ?? null,
2909
2994
  allowed_statuses: linkStatusAllowedStatuses(check) || ["2xx", "3xx"],
2910
2995
  require_nonzero_bytes: check.require_nonzero_bytes === true,
2996
+ min_bytes: check.min_bytes ?? null,
2997
+ allowed_content_types: check.allowed_content_types ?? null,
2911
2998
  viewports: summaries,
2912
2999
  failures: failed.flatMap((summary) => Array.isArray(summary.failures)
2913
3000
  ? summary.failures.map((failure) => ({ viewport: summary.viewport || null, failure }))
@@ -4055,6 +4142,28 @@ function linkProbeAllowed(status, check) {
4055
4142
  : null;
4056
4143
  return allowed ? allowed.includes(status) : status >= 200 && status < 400;
4057
4144
  }
4145
+ function linkProbeObservedBytes(result) {
4146
+ const bytes = typeof result.bytes === "number" && Number.isFinite(result.bytes) ? result.bytes : undefined;
4147
+ const contentLength = typeof result.content_length === "number" && Number.isFinite(result.content_length) ? result.content_length : undefined;
4148
+ const observed = Math.max(bytes || 0, contentLength || 0);
4149
+ return observed > 0 ? observed : undefined;
4150
+ }
4151
+ function normalizeLinkProbeContentType(value) {
4152
+ if (typeof value !== "string") return undefined;
4153
+ const normalized = (value.split(";")[0] || "").trim().toLowerCase();
4154
+ return normalized || undefined;
4155
+ }
4156
+ function linkProbeContentTypeAllowed(result, check) {
4157
+ if (!Array.isArray(check.allowed_content_types) || !check.allowed_content_types.length) return true;
4158
+ const actual = normalizeLinkProbeContentType(result.content_type);
4159
+ if (!actual) return false;
4160
+ return check.allowed_content_types.some((contentType) => {
4161
+ const normalized = normalizeLinkProbeContentType(contentType);
4162
+ if (!normalized) return false;
4163
+ if (normalized.endsWith("/*")) return actual.startsWith(normalized.slice(0, -1));
4164
+ return actual === normalized;
4165
+ });
4166
+ }
4058
4167
  function linkProbeResponseFields(response, method) {
4059
4168
  const contentLengthHeader = response.headers && typeof response.headers.get === "function" ? response.headers.get("content-length") : null;
4060
4169
  const contentLength = contentLengthHeader && /^\d+$/.test(contentLengthHeader) ? Number(contentLengthHeader) : null;
@@ -4069,6 +4178,7 @@ function linkProbeResponseFields(response, method) {
4069
4178
  }
4070
4179
  async function probeLinkStatus(candidate, check) {
4071
4180
  const requireNonzeroBytes = check.require_nonzero_bytes === true;
4181
+ const minBytes = typeof check.min_bytes === "number" && Number.isFinite(check.min_bytes) ? Math.max(1, Math.floor(check.min_bytes)) : null;
4072
4182
  const allowGetFallback = check.allow_get_fallback !== false;
4073
4183
  const result = {
4074
4184
  url: candidate.url,
@@ -4086,7 +4196,7 @@ async function probeLinkStatus(candidate, check) {
4086
4196
  };
4087
4197
  const applyResponse = async (response, method, readBytes) => {
4088
4198
  Object.assign(result, linkProbeResponseFields(response, method));
4089
- if (readBytes || requireNonzeroBytes) {
4199
+ if (readBytes) {
4090
4200
  try {
4091
4201
  const buffer = await response.arrayBuffer();
4092
4202
  result.bytes = buffer.byteLength;
@@ -4095,7 +4205,9 @@ async function probeLinkStatus(candidate, check) {
4095
4205
  }
4096
4206
  }
4097
4207
  result.ok = linkProbeAllowed(result.status, check)
4098
- && (!requireNonzeroBytes || (typeof result.bytes === "number" ? result.bytes > 0 : typeof result.content_length === "number" && result.content_length > 0))
4208
+ && linkProbeContentTypeAllowed(result, check)
4209
+ && (!requireNonzeroBytes || ((linkProbeObservedBytes(result) || 0) > 0))
4210
+ && (minBytes === null || ((linkProbeObservedBytes(result) || 0) >= minBytes))
4099
4211
  && !result.error;
4100
4212
  };
4101
4213
  try {
@@ -4112,9 +4224,9 @@ async function probeLinkStatus(candidate, check) {
4112
4224
  method: "GET",
4113
4225
  redirect: "follow",
4114
4226
  cache: "no-store",
4115
- headers: { Range: "bytes=0-0" },
4227
+ headers: { Range: "bytes=0-" + String((minBytes || 1) - 1) },
4116
4228
  });
4117
- await applyResponse(response, "GET", requireNonzeroBytes);
4229
+ await applyResponse(response, "GET", requireNonzeroBytes || minBytes !== null);
4118
4230
  return result;
4119
4231
  } catch (error) {
4120
4232
  result.error = String(error && error.message ? error.message : error).slice(0, 500);
@@ -4122,6 +4234,30 @@ async function probeLinkStatus(candidate, check) {
4122
4234
  return result;
4123
4235
  }
4124
4236
  }
4237
+ function compactLinkProbeResults(results) {
4238
+ const allResults = Array.isArray(results) ? results : [];
4239
+ if (allResults.length <= 20) {
4240
+ return {
4241
+ result_count: allResults.length,
4242
+ stored_result_count: allResults.length,
4243
+ omitted_result_count: 0,
4244
+ omitted_success_count: 0,
4245
+ results_compacted: false,
4246
+ results: allResults,
4247
+ };
4248
+ }
4249
+ const successResults = allResults.filter((result) => result && result.ok);
4250
+ const sampledSuccesses = new Set(successResults.slice(0, 5));
4251
+ const storedResults = allResults.filter((result) => result && (!result.ok || sampledSuccesses.has(result)));
4252
+ return {
4253
+ result_count: allResults.length,
4254
+ stored_result_count: storedResults.length,
4255
+ omitted_result_count: allResults.length - storedResults.length,
4256
+ omitted_success_count: Math.max(0, successResults.length - sampledSuccesses.size),
4257
+ results_compacted: storedResults.length < allResults.length,
4258
+ results: storedResults,
4259
+ };
4260
+ }
4125
4261
  async function collectLinkStatus(check) {
4126
4262
  const selector = linkStatusSelector(check);
4127
4263
  const candidateResult = await collectLinkCandidates(selector);
@@ -4168,13 +4304,17 @@ async function collectLinkStatus(check) {
4168
4304
  status: result.status,
4169
4305
  method: result.method,
4170
4306
  error: result.error,
4171
- bytes: result.bytes == null ? result.content_length : result.bytes,
4307
+ content_type: result.content_type,
4308
+ bytes: linkProbeObservedBytes(result) || null,
4309
+ min_bytes: typeof check.min_bytes === "number" && Number.isFinite(check.min_bytes) ? check.min_bytes : null,
4310
+ allowed_content_types: Array.isArray(check.allowed_content_types) ? check.allowed_content_types : null,
4172
4311
  }));
4173
4312
  const statusCounts = {};
4174
4313
  for (const result of results) {
4175
4314
  const key = result.status == null ? "error" : String(result.status);
4176
4315
  statusCounts[key] = (statusCounts[key] || 0) + 1;
4177
4316
  }
4317
+ const compactedResults = compactLinkProbeResults(results);
4178
4318
  return {
4179
4319
  version: "riddle-proof.link-status.v1",
4180
4320
  selector,
@@ -4182,6 +4322,8 @@ async function collectLinkStatus(check) {
4182
4322
  same_origin_only: linkProbeSameOriginOnly(check),
4183
4323
  dedupe: linkProbeDedupe(check),
4184
4324
  require_nonzero_bytes: check.require_nonzero_bytes === true,
4325
+ min_bytes: typeof check.min_bytes === "number" && Number.isFinite(check.min_bytes) ? check.min_bytes : null,
4326
+ allowed_content_types: Array.isArray(check.allowed_content_types) ? check.allowed_content_types : null,
4185
4327
  allowed_statuses: Array.isArray(check.allowed_statuses) && check.allowed_statuses.length
4186
4328
  ? check.allowed_statuses
4187
4329
  : typeof check.expected_status === "number"
@@ -4194,7 +4336,7 @@ async function collectLinkStatus(check) {
4194
4336
  failed_count: failures.length,
4195
4337
  status_counts: statusCounts,
4196
4338
  failures: failures.slice(0, 20),
4197
- results,
4339
+ ...compactedResults,
4198
4340
  };
4199
4341
  }
4200
4342
  async function frameEvidence(selector) {