@riddledc/riddle-proof 0.7.127 → 0.7.129

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
@@ -612,7 +612,9 @@ a field value rather than a raw substring:
612
612
 
613
613
  JSON paths support dot keys and array indexes such as `checks[0].status`, with
614
614
  `$` as the root. Each assertion supports `exists`, `equals`, `not_equals`,
615
- `contains`, and `type`.
615
+ `contains`, and `type`. Scalar observations are recorded inline; arrays and
616
+ objects are summarized with length or key counts plus a small sample so large
617
+ proof artifacts do not get duplicated into the assertion evidence.
616
618
 
617
619
  `body_contains`, `body_patterns`, `body_not_contains`, and
618
620
  `body_not_patterns` match the raw HTTP response body, not rendered browser
@@ -244,6 +244,37 @@ function jsonValueType(value) {
244
244
  if (typeof value === "string") return "string";
245
245
  return "object";
246
246
  }
247
+ function compactJsonAssertionSample(value, depth = 0) {
248
+ if (typeof value === "string") return value.length > 240 ? `${value.slice(0, 237)}...` : value;
249
+ if (value === null || typeof value === "boolean" || typeof value === "number") return toJsonValue(value);
250
+ if (Array.isArray(value)) {
251
+ if (depth >= 2) return `[array:${value.length}]`;
252
+ return value.slice(0, 3).map((item) => compactJsonAssertionSample(item, depth + 1));
253
+ }
254
+ if (isRecord(value)) {
255
+ const entries = Object.entries(value).slice(0, 8);
256
+ if (depth >= 2) return `[object:${Object.keys(value).length} keys]`;
257
+ return Object.fromEntries(entries.map(([key, child]) => [key, compactJsonAssertionSample(child, depth + 1)]));
258
+ }
259
+ return String(value);
260
+ }
261
+ function attachJsonAssertionObservedValue(result, value) {
262
+ const type = jsonValueType(value);
263
+ if (type === "array" && Array.isArray(value)) {
264
+ result.observed_length = value.length;
265
+ result.observed_omitted_count = Math.max(0, value.length - 3);
266
+ result.observed_sample = compactJsonAssertionSample(value);
267
+ return;
268
+ }
269
+ if (type === "object" && isRecord(value)) {
270
+ const keyCount = Object.keys(value).length;
271
+ result.observed_key_count = keyCount;
272
+ result.observed_omitted_count = Math.max(0, keyCount - 8);
273
+ result.observed_sample = compactJsonAssertionSample(value);
274
+ return;
275
+ }
276
+ result.observed = toJsonValue(value);
277
+ }
247
278
  function deepJsonEqual(left, right) {
248
279
  if (left === right) return true;
249
280
  if (typeof left !== typeof right) return false;
@@ -341,7 +372,7 @@ function evaluateHttpStatusBodyJsonAssertion(root, assertion) {
341
372
  exists: resolved.exists,
342
373
  observed_type: resolved.exists ? jsonValueType(resolved.value) : "missing"
343
374
  };
344
- if (resolved.exists) result.observed = toJsonValue(resolved.value);
375
+ if (resolved.exists) attachJsonAssertionObservedValue(result, resolved.value);
345
376
  if (resolved.error) errors.push(resolved.error);
346
377
  if (hasOwn(assertion, "exists")) {
347
378
  result.expected_exists = assertion.exists;
@@ -1434,6 +1465,10 @@ function httpStatusBodyJsonAssertionFailures(result, check) {
1434
1465
  ok: false,
1435
1466
  exists: assertion.exists === true,
1436
1467
  observed: hasOwn(assertion, "observed") ? toJsonValue(assertion.observed) : void 0,
1468
+ observed_sample: hasOwn(assertion, "observed_sample") ? toJsonValue(assertion.observed_sample) : void 0,
1469
+ observed_length: numberValue(assertion.observed_length),
1470
+ observed_key_count: numberValue(assertion.observed_key_count),
1471
+ observed_omitted_count: numberValue(assertion.observed_omitted_count),
1437
1472
  observed_type: stringValue(assertion.observed_type) || "missing",
1438
1473
  expected_exists: booleanValue(assertion.expected_exists),
1439
1474
  equals: hasOwn(assertion, "equals") ? toJsonValue(assertion.equals) : void 0,
@@ -1819,6 +1854,49 @@ function matchText(sample, check) {
1819
1854
  }
1820
1855
  return sample.includes(check.text || "");
1821
1856
  }
1857
+ function compactTextEvidenceSample(value) {
1858
+ return String(value || "").replace(/\s+/g, " ").trim();
1859
+ }
1860
+ function textSampleAroundMatch(sample, index, length) {
1861
+ if (index < 0) return void 0;
1862
+ const source = String(sample || "");
1863
+ const context = 120;
1864
+ const start = Math.max(0, index - context);
1865
+ const end = Math.min(source.length, index + Math.max(length, 1) + context);
1866
+ const prefix = start > 0 ? "..." : "";
1867
+ const suffix = end < source.length ? "..." : "";
1868
+ const compacted = compactTextEvidenceSample(`${prefix}${source.slice(start, end)}${suffix}`);
1869
+ return compacted ? compacted.slice(0, 240) : void 0;
1870
+ }
1871
+ function textMatchSamples(sample, check) {
1872
+ const source = String(sample || "");
1873
+ if (!source) return [];
1874
+ if (check.pattern) {
1875
+ try {
1876
+ const flags = Array.from(new Set(String(check.flags || "").replace(/[gy]/g, "").split(""))).join("");
1877
+ const match = new RegExp(check.pattern, flags).exec(source);
1878
+ const sampleText2 = match ? textSampleAroundMatch(source, match.index, match[0]?.length || 1) : void 0;
1879
+ return sampleText2 ? [sampleText2] : [];
1880
+ } catch {
1881
+ return [];
1882
+ }
1883
+ }
1884
+ const text = check.text || "";
1885
+ if (!text) return [];
1886
+ const index = source.indexOf(text);
1887
+ const sampleText = textSampleAroundMatch(source, index, text.length);
1888
+ return sampleText ? [sampleText] : [];
1889
+ }
1890
+ function textCheckFailureSamples(viewport, check) {
1891
+ const key = textKey(check);
1892
+ const captured = viewport.text_match_samples?.[key] || [];
1893
+ const capturedSamples = captured.map((sample) => compactTextEvidenceSample(sample).slice(0, 240)).filter(Boolean);
1894
+ if (capturedSamples.length) return capturedSamples.slice(0, 3);
1895
+ const matchedSamples = textMatchSamples(viewport.body_text_sample || "", check);
1896
+ if (matchedSamples.length) return matchedSamples.slice(0, 3);
1897
+ const fallback = compactTextEvidenceSample(viewport.body_text_sample || "").slice(0, 240);
1898
+ return fallback ? [fallback] : [];
1899
+ }
1822
1900
  function allowedMessageSample(input) {
1823
1901
  if (!isRecord(input)) return String(input || "");
1824
1902
  const parts = [
@@ -2263,10 +2341,17 @@ function assessCheckFromEvidence(check, evidence) {
2263
2341
  if (check.type === "text_visible" || check.type === "text_absent") {
2264
2342
  const key = textKey(check);
2265
2343
  const expectedVisible = check.type === "text_visible";
2266
- const matches = viewports.map((viewport) => {
2344
+ const results = viewports.map((viewport) => {
2267
2345
  const fromEvidence = viewport.text_matches?.[key];
2268
- return typeof fromEvidence === "boolean" ? fromEvidence : matchText(viewport.body_text_sample || "", check);
2346
+ const matched = typeof fromEvidence === "boolean" ? fromEvidence : matchText(viewport.body_text_sample || "", check);
2347
+ const failedAgainstExpectation = matched !== expectedVisible;
2348
+ return {
2349
+ viewport: viewport.name,
2350
+ matched,
2351
+ samples: failedAgainstExpectation ? textCheckFailureSamples(viewport, check) : []
2352
+ };
2269
2353
  });
2354
+ const matches = results.map((result) => result.matched);
2270
2355
  const failed = matches.filter((matched) => matched !== expectedVisible).length;
2271
2356
  return {
2272
2357
  type: check.type,
@@ -2275,7 +2360,8 @@ function assessCheckFromEvidence(check, evidence) {
2275
2360
  evidence: {
2276
2361
  text: check.text || null,
2277
2362
  pattern: check.pattern || null,
2278
- matches
2363
+ matches,
2364
+ viewports: results.map((result) => toJsonValue(result))
2279
2365
  },
2280
2366
  message: failed ? `Text assertion failed in ${failed} viewport(s).` : void 0
2281
2367
  };
@@ -2862,6 +2948,48 @@ function textMatches(sample, check) {
2862
2948
  }
2863
2949
  return String(sample || "").includes(check.text || "");
2864
2950
  }
2951
+ function compactTextEvidenceSample(value) {
2952
+ return String(value || "").replace(/\s+/g, " ").trim();
2953
+ }
2954
+ function textSampleAroundMatch(sample, index, length) {
2955
+ if (index < 0) return undefined;
2956
+ const source = String(sample || "");
2957
+ const context = 120;
2958
+ const start = Math.max(0, index - context);
2959
+ const end = Math.min(source.length, index + Math.max(length, 1) + context);
2960
+ const prefix = start > 0 ? "..." : "";
2961
+ const suffix = end < source.length ? "..." : "";
2962
+ const compacted = compactTextEvidenceSample(prefix + source.slice(start, end) + suffix);
2963
+ return compacted ? compacted.slice(0, 240) : undefined;
2964
+ }
2965
+ function textMatchSamples(sample, check) {
2966
+ const source = String(sample || "");
2967
+ if (!source) return [];
2968
+ if (check.pattern) {
2969
+ try {
2970
+ const flags = Array.from(new Set(String(check.flags || "").replace(/[gy]/g, "").split(""))).join("");
2971
+ const match = new RegExp(check.pattern, flags).exec(source);
2972
+ const sampleText = match ? textSampleAroundMatch(source, match.index, match[0] ? match[0].length : 1) : undefined;
2973
+ return sampleText ? [sampleText] : [];
2974
+ } catch { return []; }
2975
+ }
2976
+ const text = check.text || "";
2977
+ if (!text) return [];
2978
+ const sampleText = textSampleAroundMatch(source, source.indexOf(text), text.length);
2979
+ return sampleText ? [sampleText] : [];
2980
+ }
2981
+ function textCheckFailureSamples(viewport, check) {
2982
+ const key = check.pattern ? "pattern:" + check.pattern + "/" + (check.flags || "") : "text:" + (check.text || "");
2983
+ const captured = viewport && viewport.text_match_samples && Array.isArray(viewport.text_match_samples[key]) ? viewport.text_match_samples[key] : [];
2984
+ const capturedSamples = captured
2985
+ .map((sample) => compactTextEvidenceSample(sample).slice(0, 240))
2986
+ .filter(Boolean);
2987
+ if (capturedSamples.length) return capturedSamples.slice(0, 3);
2988
+ const matchedSamples = textMatchSamples(viewport && viewport.body_text_sample || "", check);
2989
+ if (matchedSamples.length) return matchedSamples.slice(0, 3);
2990
+ const fallback = compactTextEvidenceSample(viewport && viewport.body_text_sample || "").slice(0, 240);
2991
+ return fallback ? [fallback] : [];
2992
+ }
2865
2993
  function allowedMessageSample(input) {
2866
2994
  if (!input || typeof input !== "object" || Array.isArray(input)) return String(input || "");
2867
2995
  const parts = [
@@ -3064,6 +3192,10 @@ function httpStatusBodyJsonAssertionFailures(result, check) {
3064
3192
  ok: false,
3065
3193
  exists: assertion.exists === true,
3066
3194
  observed: Object.hasOwn(assertion, "observed") ? assertion.observed : undefined,
3195
+ observed_sample: Object.hasOwn(assertion, "observed_sample") ? assertion.observed_sample : undefined,
3196
+ observed_length: typeof assertion.observed_length === "number" && Number.isFinite(assertion.observed_length) ? assertion.observed_length : undefined,
3197
+ observed_key_count: typeof assertion.observed_key_count === "number" && Number.isFinite(assertion.observed_key_count) ? assertion.observed_key_count : undefined,
3198
+ observed_omitted_count: typeof assertion.observed_omitted_count === "number" && Number.isFinite(assertion.observed_omitted_count) ? assertion.observed_omitted_count : undefined,
3067
3199
  observed_type: typeof assertion.observed_type === "string" && assertion.observed_type ? assertion.observed_type : "missing",
3068
3200
  expected_exists: typeof assertion.expected_exists === "boolean" ? assertion.expected_exists : undefined,
3069
3201
  equals: Object.hasOwn(assertion, "equals") ? assertion.equals : undefined,
@@ -3935,13 +4067,22 @@ function assessProfile(profile, evidence) {
3935
4067
  if (check.type === "text_visible" || check.type === "text_absent") {
3936
4068
  const key = check.pattern ? "pattern:" + check.pattern + "/" + (check.flags || "") : "text:" + (check.text || "");
3937
4069
  const expectedVisible = check.type === "text_visible";
3938
- const matches = checkViewports.map((viewport) => viewport.text_matches && typeof viewport.text_matches[key] === "boolean" ? viewport.text_matches[key] : textMatches(viewport.body_text_sample || "", check));
4070
+ const results = checkViewports.map((viewport) => {
4071
+ const matched = viewport.text_matches && typeof viewport.text_matches[key] === "boolean" ? viewport.text_matches[key] : textMatches(viewport.body_text_sample || "", check);
4072
+ const failedAgainstExpectation = matched !== expectedVisible;
4073
+ return {
4074
+ viewport: viewport.name,
4075
+ matched,
4076
+ samples: failedAgainstExpectation ? textCheckFailureSamples(viewport, check) : [],
4077
+ };
4078
+ });
4079
+ const matches = results.map((result) => result.matched);
3939
4080
  const failed = matches.filter((matched) => matched !== expectedVisible).length;
3940
4081
  checks.push({
3941
4082
  type: check.type,
3942
4083
  label: check.label || check.type,
3943
4084
  status: failed ? "failed" : "passed",
3944
- evidence: { text: check.text, pattern: check.pattern, matches },
4085
+ evidence: { text: check.text, pattern: check.pattern, matches, viewports: results },
3945
4086
  message: failed ? "Text assertion failed in " + failed + " viewport(s)." : undefined,
3946
4087
  });
3947
4088
  continue;
@@ -4261,6 +4402,36 @@ function textMatches(sample, check) {
4261
4402
  }
4262
4403
  return String(sample || "").includes(check.text || "");
4263
4404
  }
4405
+ function compactTextEvidenceSample(value) {
4406
+ return String(value || "").replace(/\s+/g, " ").trim();
4407
+ }
4408
+ function textSampleAroundMatch(sample, index, length) {
4409
+ if (index < 0) return undefined;
4410
+ const source = String(sample || "");
4411
+ const context = 120;
4412
+ const start = Math.max(0, index - context);
4413
+ const end = Math.min(source.length, index + Math.max(length, 1) + context);
4414
+ const prefix = start > 0 ? "..." : "";
4415
+ const suffix = end < source.length ? "..." : "";
4416
+ const compacted = compactTextEvidenceSample(prefix + source.slice(start, end) + suffix);
4417
+ return compacted ? compacted.slice(0, 240) : undefined;
4418
+ }
4419
+ function textMatchSamples(sample, check) {
4420
+ const source = String(sample || "");
4421
+ if (!source) return [];
4422
+ if (check.pattern) {
4423
+ try {
4424
+ const flags = Array.from(new Set(String(check.flags || "").replace(/[gy]/g, "").split(""))).join("");
4425
+ const match = new RegExp(check.pattern, flags).exec(source);
4426
+ const sampleText = match ? textSampleAroundMatch(source, match.index, match[0] ? match[0].length : 1) : undefined;
4427
+ return sampleText ? [sampleText] : [];
4428
+ } catch { return []; }
4429
+ }
4430
+ const text = check.text || "";
4431
+ if (!text) return [];
4432
+ const sampleText = textSampleAroundMatch(source, source.indexOf(text), text.length);
4433
+ return sampleText ? [sampleText] : [];
4434
+ }
4264
4435
  function profileCheckAppliesToViewport(check, viewport) {
4265
4436
  if (!Array.isArray(check.viewports) || !check.viewports.length) return true;
4266
4437
  return Boolean(viewport && viewport.name && check.viewports.includes(viewport.name));
@@ -5271,6 +5442,38 @@ function jsonProbeValueType(value) {
5271
5442
  if (typeof value === "string") return "string";
5272
5443
  return "object";
5273
5444
  }
5445
+ function compactJsonProbeSample(value, depth) {
5446
+ const level = typeof depth === "number" ? depth : 0;
5447
+ if (typeof value === "string") return value.length > 240 ? value.slice(0, 237) + "..." : value;
5448
+ if (value === null || typeof value === "boolean" || typeof value === "number") return value;
5449
+ if (Array.isArray(value)) {
5450
+ if (level >= 2) return "[array:" + value.length + "]";
5451
+ return value.slice(0, 3).map((item) => compactJsonProbeSample(item, level + 1));
5452
+ }
5453
+ if (value && typeof value === "object") {
5454
+ const entries = Object.entries(value);
5455
+ if (level >= 2) return "[object:" + entries.length + " keys]";
5456
+ return Object.fromEntries(entries.slice(0, 8).map(([key, child]) => [key, compactJsonProbeSample(child, level + 1)]));
5457
+ }
5458
+ return String(value);
5459
+ }
5460
+ function attachJsonProbeObservedValue(result, value) {
5461
+ const type = jsonProbeValueType(value);
5462
+ if (type === "array" && Array.isArray(value)) {
5463
+ result.observed_length = value.length;
5464
+ result.observed_omitted_count = Math.max(0, value.length - 3);
5465
+ result.observed_sample = compactJsonProbeSample(value, 0);
5466
+ return;
5467
+ }
5468
+ if (type === "object" && value && typeof value === "object" && !Array.isArray(value)) {
5469
+ const keyCount = Object.keys(value).length;
5470
+ result.observed_key_count = keyCount;
5471
+ result.observed_omitted_count = Math.max(0, keyCount - 8);
5472
+ result.observed_sample = compactJsonProbeSample(value, 0);
5473
+ return;
5474
+ }
5475
+ result.observed = value;
5476
+ }
5274
5477
  function jsonProbeDeepEqual(left, right) {
5275
5478
  if (left === right) return true;
5276
5479
  if (typeof left !== typeof right) return false;
@@ -5367,7 +5570,7 @@ function evaluateJsonProbeAssertion(root, assertion) {
5367
5570
  exists: resolved.exists,
5368
5571
  observed_type: resolved.exists ? jsonProbeValueType(resolved.value) : "missing",
5369
5572
  };
5370
- if (resolved.exists) result.observed = resolved.value;
5573
+ if (resolved.exists) attachJsonProbeObservedValue(result, resolved.value);
5371
5574
  if (resolved.error) errors.push(resolved.error);
5372
5575
  if (Object.hasOwn(assertion, "exists")) {
5373
5576
  result.expected_exists = assertion.exists;
@@ -6180,6 +6383,7 @@ async function captureViewport(viewport) {
6180
6383
  const frames = {};
6181
6384
  const text_sequences = {};
6182
6385
  const text_matches = {};
6386
+ const text_match_samples = {};
6183
6387
  const http_statuses = {};
6184
6388
  const link_statuses = {};
6185
6389
  for (const check of profile.checks || []) {
@@ -6201,7 +6405,10 @@ async function captureViewport(viewport) {
6201
6405
  text_sequences[check.selector] = await selectorTextSequence(check.selector);
6202
6406
  }
6203
6407
  if ((check.type === "text_visible" || check.type === "text_absent") && (check.text || check.pattern)) {
6204
- text_matches[textKey(check)] = textMatches(dom.body_text || dom.body_text_sample || "", check);
6408
+ const key = textKey(check);
6409
+ const sample = dom.body_text || dom.body_text_sample || "";
6410
+ text_matches[key] = textMatches(sample, check);
6411
+ text_match_samples[key] = textMatchSamples(sample, check);
6205
6412
  }
6206
6413
  if ((check.type === "frame_text_visible" || check.type === "frame_url_equals" || check.type === "frame_url_matches" || check.type === "frame_no_horizontal_overflow") && check.selector) {
6207
6414
  selectors[check.selector] = selectors[check.selector] || await selectorStats(check.selector);
@@ -6278,6 +6485,7 @@ async function captureViewport(viewport) {
6278
6485
  frames,
6279
6486
  text_sequences,
6280
6487
  text_matches,
6488
+ text_match_samples,
6281
6489
  http_statuses,
6282
6490
  link_statuses,
6283
6491
  route_inventory: routeInventory,