harnery 0.31.5 → 0.32.0

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.
Files changed (222) hide show
  1. package/dist/commander.d.ts.map +1 -1
  2. package/dist/commander.js +2 -0
  3. package/dist/commands/agents.d.ts +0 -9
  4. package/dist/commands/agents.d.ts.map +1 -1
  5. package/dist/commands/agents.js +70 -95
  6. package/dist/commands/browse-session.d.ts +21 -0
  7. package/dist/commands/browse-session.d.ts.map +1 -0
  8. package/dist/commands/browse-session.js +157 -0
  9. package/dist/commands/browse.d.ts.map +1 -1
  10. package/dist/commands/browse.js +205 -37
  11. package/dist/commands/checkpoint.js +1 -1
  12. package/dist/commands/deinit.d.ts.map +1 -1
  13. package/dist/commands/deinit.js +4 -0
  14. package/dist/commands/docs.d.ts.map +1 -1
  15. package/dist/commands/docs.js +40 -0
  16. package/dist/commands/doctor.d.ts.map +1 -1
  17. package/dist/commands/doctor.js +100 -17
  18. package/dist/commands/init.d.ts +1 -2
  19. package/dist/commands/init.d.ts.map +1 -1
  20. package/dist/commands/init.js +16 -5
  21. package/dist/commands/tunnel.d.ts.map +1 -1
  22. package/dist/commands/tunnel.js +2 -3
  23. package/dist/core/agents/canonical-emit.d.ts +1 -2
  24. package/dist/core/agents/canonical-emit.d.ts.map +1 -1
  25. package/dist/core/agents/canonical-emit.js +1 -2
  26. package/dist/core/agents/cli.js +123 -88
  27. package/dist/core/agents/coord-client.d.ts +11 -4
  28. package/dist/core/agents/coord-client.d.ts.map +1 -1
  29. package/dist/core/agents/coord-client.js +27 -9
  30. package/dist/core/agents/finalization.d.ts +68 -0
  31. package/dist/core/agents/finalization.d.ts.map +1 -0
  32. package/dist/core/agents/finalization.js +443 -0
  33. package/dist/core/agents/git-hook.d.ts +51 -0
  34. package/dist/core/agents/git-hook.d.ts.map +1 -0
  35. package/dist/core/agents/git-hook.js +118 -0
  36. package/dist/core/agents/render/prompt-context.d.ts +6 -5
  37. package/dist/core/agents/render/prompt-context.d.ts.map +1 -1
  38. package/dist/core/agents/render/prompt-context.js +26 -13
  39. package/dist/core/agents/render/session-context.d.ts.map +1 -1
  40. package/dist/core/agents/render/session-context.js +15 -2
  41. package/dist/core/agents/rules/claim-conflict.js +3 -3
  42. package/dist/core/agents/rules/commit-conflict.d.ts +15 -6
  43. package/dist/core/agents/rules/commit-conflict.d.ts.map +1 -1
  44. package/dist/core/agents/rules/commit-conflict.js +21 -5
  45. package/dist/core/agents/rules/stop-hook.d.ts +3 -0
  46. package/dist/core/agents/rules/stop-hook.d.ts.map +1 -1
  47. package/dist/core/agents/rules/stop-hook.js +59 -23
  48. package/dist/core/agents/session-events.d.ts +8 -16
  49. package/dist/core/agents/session-events.d.ts.map +1 -1
  50. package/dist/core/agents/session-events.js +12 -26
  51. package/dist/core/agents/state/heartbeat-projector.d.ts +3 -0
  52. package/dist/core/agents/state/heartbeat-projector.d.ts.map +1 -1
  53. package/dist/core/agents/state/heartbeat-projector.js +44 -1
  54. package/dist/core/agents/state/heartbeat-writer.d.ts +32 -5
  55. package/dist/core/agents/state/heartbeat-writer.d.ts.map +1 -1
  56. package/dist/core/agents/state/heartbeat-writer.js +54 -17
  57. package/dist/core/agents/state/names.d.ts +22 -1
  58. package/dist/core/agents/state/names.d.ts.map +1 -1
  59. package/dist/core/agents/state/names.js +40 -2
  60. package/dist/core/config.d.ts +30 -2
  61. package/dist/core/config.d.ts.map +1 -1
  62. package/dist/core/config.js +74 -11
  63. package/dist/core/governor/planning.d.ts.map +1 -1
  64. package/dist/core/governor/planning.js +12 -13
  65. package/dist/core/hooks/adapter/detect.d.ts +2 -4
  66. package/dist/core/hooks/adapter/detect.d.ts.map +1 -1
  67. package/dist/core/hooks/adapter/detect.js +4 -14
  68. package/dist/core/hooks/adapter/output.d.ts +1 -1
  69. package/dist/core/hooks/adapter/output.d.ts.map +1 -1
  70. package/dist/core/hooks/adapter/output.js +5 -4
  71. package/dist/core/hooks/adapter/parse.d.ts +1 -1
  72. package/dist/core/hooks/adapter/parse.d.ts.map +1 -1
  73. package/dist/core/hooks/adapter/parse.js +22 -5
  74. package/dist/core/hooks/adapter/wiring.d.ts +23 -0
  75. package/dist/core/hooks/adapter/wiring.d.ts.map +1 -1
  76. package/dist/core/hooks/adapter/wiring.js +32 -0
  77. package/dist/core/hooks/cli.d.ts +1 -2
  78. package/dist/core/hooks/cli.d.ts.map +1 -1
  79. package/dist/core/hooks/cli.js +169 -27
  80. package/dist/core/hooks/codex-wsl-bridge.d.ts +46 -0
  81. package/dist/core/hooks/codex-wsl-bridge.d.ts.map +1 -0
  82. package/dist/core/hooks/codex-wsl-bridge.js +137 -0
  83. package/dist/core/hooks/effects/index.d.ts +1 -1
  84. package/dist/core/hooks/effects/index.js +3 -3
  85. package/dist/core/hooks/events/schema.d.ts +27 -0
  86. package/dist/core/hooks/events/schema.d.ts.map +1 -1
  87. package/dist/core/hooks/resolve/owner.d.ts +7 -1
  88. package/dist/core/hooks/resolve/owner.d.ts.map +1 -1
  89. package/dist/core/hooks/resolve/owner.js +15 -0
  90. package/dist/core/hooks/resolve/transcript.d.ts +44 -0
  91. package/dist/core/hooks/resolve/transcript.d.ts.map +1 -1
  92. package/dist/core/hooks/resolve/transcript.js +176 -1
  93. package/dist/core/hooks/session-name-presence.d.ts +30 -0
  94. package/dist/core/hooks/session-name-presence.d.ts.map +1 -0
  95. package/dist/core/hooks/session-name-presence.js +43 -0
  96. package/dist/core/hooks/unsafe-cross-shell.d.ts +13 -0
  97. package/dist/core/hooks/unsafe-cross-shell.d.ts.map +1 -0
  98. package/dist/core/hooks/unsafe-cross-shell.js +151 -0
  99. package/dist/core/work/state.d.ts +4 -5
  100. package/dist/core/work/state.d.ts.map +1 -1
  101. package/dist/core/work/state.js +5 -8
  102. package/dist/core/workflow/index.d.ts +1 -1
  103. package/dist/core/workflow/index.d.ts.map +1 -1
  104. package/dist/core/workflow/proof.d.ts +2 -2
  105. package/dist/core/workflow/proof.d.ts.map +1 -1
  106. package/dist/core/workflow/proof.js +2 -2
  107. package/dist/core/workflow/run-state.d.ts +2 -2
  108. package/dist/core/workflow/run-state.d.ts.map +1 -1
  109. package/dist/core/workflow/run-state.js +2 -2
  110. package/dist/core/workflow/types.d.ts +3 -3
  111. package/dist/core/workflow/types.d.ts.map +1 -1
  112. package/dist/core/workflow/workspaces/execution.d.ts +2 -2
  113. package/dist/core/workflow/workspaces/execution.d.ts.map +1 -1
  114. package/dist/core/workflow/workspaces/execution.js +5 -0
  115. package/dist/core/workflow/workspaces/index.d.ts +1 -1
  116. package/dist/core/workflow/workspaces/index.d.ts.map +1 -1
  117. package/dist/core/workflow/workspaces/local-git.d.ts.map +1 -1
  118. package/dist/core/workflow/workspaces/local-git.js +1 -2
  119. package/dist/core/workflow/workspaces/paths.d.ts +2 -2
  120. package/dist/core/workflow/workspaces/types.d.ts +0 -6
  121. package/dist/core/workflow/workspaces/types.d.ts.map +1 -1
  122. package/dist/core/workflow/workspaces/validate.d.ts +0 -2
  123. package/dist/core/workflow/workspaces/validate.d.ts.map +1 -1
  124. package/dist/core/workflow/workspaces/validate.js +0 -2
  125. package/dist/lib/browser/client.d.ts +118 -1
  126. package/dist/lib/browser/client.d.ts.map +1 -1
  127. package/dist/lib/browser/client.js +435 -6
  128. package/dist/lib/browser/geometry.d.ts.map +1 -1
  129. package/dist/lib/browser/geometry.js +180 -37
  130. package/dist/lib/browser/index.d.ts +5 -1
  131. package/dist/lib/browser/index.d.ts.map +1 -1
  132. package/dist/lib/browser/index.js +5 -1
  133. package/dist/lib/browser/netscape-cookies.d.ts +6 -0
  134. package/dist/lib/browser/netscape-cookies.d.ts.map +1 -0
  135. package/dist/lib/browser/netscape-cookies.js +31 -0
  136. package/dist/lib/browser/proxy.d.ts +24 -0
  137. package/dist/lib/browser/proxy.d.ts.map +1 -0
  138. package/dist/lib/browser/proxy.js +84 -0
  139. package/dist/lib/browser/runts.d.ts +6 -0
  140. package/dist/lib/browser/runts.d.ts.map +1 -1
  141. package/dist/lib/browser/runts.js +21 -4
  142. package/dist/lib/browser/session-control.d.ts +112 -0
  143. package/dist/lib/browser/session-control.d.ts.map +1 -0
  144. package/dist/lib/browser/session-control.js +670 -0
  145. package/dist/lib/docs-links.d.ts +108 -0
  146. package/dist/lib/docs-links.d.ts.map +1 -0
  147. package/dist/lib/docs-links.js +555 -0
  148. package/dist/lib/exec.d.ts +1 -1
  149. package/dist/lib/exec.js +1 -1
  150. package/dist/lib/identities/assume.d.ts +4 -2
  151. package/dist/lib/identities/assume.d.ts.map +1 -1
  152. package/dist/lib/identities/assume.js +15 -2
  153. package/dist/lib/instructions/git-hooks.d.ts +75 -0
  154. package/dist/lib/instructions/git-hooks.d.ts.map +1 -0
  155. package/dist/lib/instructions/git-hooks.js +238 -0
  156. package/dist/lib/instructions/splice.d.ts +10 -4
  157. package/dist/lib/instructions/splice.d.ts.map +1 -1
  158. package/dist/lib/instructions/splice.js +19 -11
  159. package/package.json +1 -1
  160. package/schemas/config.schema.json +32 -0
  161. package/src/commander.ts +2 -0
  162. package/src/commands/agents.ts +96 -115
  163. package/src/commands/browse-session.ts +245 -0
  164. package/src/commands/browse.ts +287 -44
  165. package/src/commands/checkpoint.ts +1 -1
  166. package/src/commands/deinit.ts +7 -1
  167. package/src/commands/docs.ts +56 -0
  168. package/src/commands/doctor.ts +100 -18
  169. package/src/commands/init.ts +17 -9
  170. package/src/commands/tunnel.ts +2 -5
  171. package/src/core/agents/canonical-emit.ts +1 -2
  172. package/src/core/agents/cli.ts +137 -88
  173. package/src/core/agents/coord-client.ts +38 -9
  174. package/src/core/agents/finalization.ts +595 -0
  175. package/src/core/agents/git-hook.ts +126 -0
  176. package/src/core/agents/render/prompt-context.ts +28 -13
  177. package/src/core/agents/render/session-context.ts +15 -2
  178. package/src/core/agents/rules/claim-conflict.ts +3 -3
  179. package/src/core/agents/rules/commit-conflict.ts +35 -8
  180. package/src/core/agents/rules/stop-hook.ts +71 -23
  181. package/src/core/agents/session-events.ts +14 -41
  182. package/src/core/agents/state/heartbeat-projector.ts +45 -1
  183. package/src/core/agents/state/heartbeat-writer.ts +78 -16
  184. package/src/core/agents/state/names.ts +54 -2
  185. package/src/core/config.ts +92 -12
  186. package/src/core/governor/planning.ts +17 -12
  187. package/src/core/hooks/adapter/detect.ts +4 -13
  188. package/src/core/hooks/adapter/output.ts +9 -4
  189. package/src/core/hooks/adapter/parse.ts +24 -5
  190. package/src/core/hooks/adapter/wiring.ts +43 -0
  191. package/src/core/hooks/cli.ts +201 -23
  192. package/src/core/hooks/codex-wsl-bridge.ts +188 -0
  193. package/src/core/hooks/effects/index.ts +5 -5
  194. package/src/core/hooks/events/schema.ts +27 -0
  195. package/src/core/hooks/resolve/owner.ts +22 -1
  196. package/src/core/hooks/resolve/transcript.ts +164 -1
  197. package/src/core/hooks/session-name-presence.ts +52 -0
  198. package/src/core/hooks/unsafe-cross-shell.ts +160 -0
  199. package/src/core/work/state.ts +8 -12
  200. package/src/core/workflow/engine.ts +2 -2
  201. package/src/core/workflow/index.ts +0 -1
  202. package/src/core/workflow/proof.ts +4 -4
  203. package/src/core/workflow/run-state.ts +5 -5
  204. package/src/core/workflow/types.ts +3 -3
  205. package/src/core/workflow/workspaces/execution.ts +11 -4
  206. package/src/core/workflow/workspaces/index.ts +0 -3
  207. package/src/core/workflow/workspaces/local-git.ts +1 -2
  208. package/src/core/workflow/workspaces/paths.ts +2 -2
  209. package/src/core/workflow/workspaces/types.ts +0 -9
  210. package/src/core/workflow/workspaces/validate.ts +1 -5
  211. package/src/lib/browser/client.ts +528 -6
  212. package/src/lib/browser/geometry.ts +197 -37
  213. package/src/lib/browser/index.ts +39 -0
  214. package/src/lib/browser/netscape-cookies.ts +39 -0
  215. package/src/lib/browser/proxy.ts +105 -0
  216. package/src/lib/browser/runts.ts +27 -3
  217. package/src/lib/browser/session-control.ts +892 -0
  218. package/src/lib/docs-links.ts +674 -0
  219. package/src/lib/exec.ts +1 -1
  220. package/src/lib/identities/assume.ts +22 -1
  221. package/src/lib/instructions/git-hooks.ts +259 -0
  222. package/src/lib/instructions/splice.ts +41 -11
@@ -32,8 +32,12 @@ export function intersectRects(a, b) {
32
32
  */
33
33
  export function buildLayoutLintCheck() {
34
34
  return (request) => {
35
- const CHILD_LIMIT = 250;
35
+ const CHILD_LIMIT = 1_000;
36
+ const TEXT_LIMIT = 1_000;
36
37
  const ISSUE_LIMIT = 100;
38
+ // Chromium geometry is fractional even when authored CSS is integral.
39
+ // Differences below half a CSS pixel are rasterization noise, not clipping.
40
+ const SUBPIXEL_EPSILON = 0.5;
37
41
  const rectOf = (rect) => ({
38
42
  x: rect.x,
39
43
  y: rect.y,
@@ -65,6 +69,14 @@ export function buildLayoutLintCheck() {
65
69
  const isHidden = (element) => {
66
70
  let current = element;
67
71
  while (current) {
72
+ // A collapsed disclosure hides its content through the ::details-content
73
+ // pseudo, which is not in the ancestor chain, so no computed style on a
74
+ // real ancestor reports it. Without this, every closed accordion panel
75
+ // reads as text clipped out of its container.
76
+ const parent = current.parentElement;
77
+ if (parent instanceof HTMLDetailsElement && !parent.open && current.tagName !== "SUMMARY") {
78
+ return true;
79
+ }
68
80
  const style = getComputedStyle(current);
69
81
  const contentVisibility = style
70
82
  .contentVisibility;
@@ -300,6 +312,27 @@ export function buildLayoutLintCheck() {
300
312
  const clips = new Set(["hidden", "clip", "auto", "scroll"]);
301
313
  return { x: clips.has(style.overflowX), y: clips.has(style.overflowY) };
302
314
  };
315
+ // A scroller hides content the same way a clip does, but the reader can
316
+ // reach it. Content below the fold of a capped table or right of a wide
317
+ // code block is not a layout defect, so an axis stops being checked at the
318
+ // first ancestor that scrolls it. Constraints from ancestors INSIDE that
319
+ // scroller still apply -- an overflow:hidden box within a scroller really
320
+ // does cut its content off, and the scroller cannot reveal it.
321
+ const scrollingStyle = (element) => {
322
+ const style = getComputedStyle(element);
323
+ const scrolls = new Set(["auto", "scroll"]);
324
+ return { x: scrolls.has(style.overflowX), y: scrolls.has(style.overflowY) };
325
+ };
326
+ const unbounded = () => ({
327
+ x: Number.NEGATIVE_INFINITY,
328
+ y: Number.NEGATIVE_INFINITY,
329
+ left: Number.NEGATIVE_INFINITY,
330
+ top: Number.NEGATIVE_INFINITY,
331
+ right: Number.POSITIVE_INFINITY,
332
+ bottom: Number.POSITIVE_INFINITY,
333
+ width: Number.POSITIVE_INFINITY,
334
+ height: Number.POSITIVE_INFINITY,
335
+ });
303
336
  const paddingRect = (element) => {
304
337
  const rect = element.getBoundingClientRect();
305
338
  const html = element;
@@ -319,8 +352,8 @@ export function buildLayoutLintCheck() {
319
352
  };
320
353
  };
321
354
  const clip = request.clip.map(({ selector, tolerancePx }) => {
322
- const container = document.querySelector(selector);
323
- if (!(container instanceof Element)) {
355
+ const containers = [...document.querySelectorAll(selector)];
356
+ if (containers.length === 0) {
324
357
  return {
325
358
  rule: "clip",
326
359
  selector,
@@ -336,31 +369,47 @@ export function buildLayoutLintCheck() {
336
369
  const unsupported = new Set();
337
370
  const excluded = [];
338
371
  const issues = [];
339
- const descendants = [...container.querySelectorAll("*")];
340
- let truncated = descendants.length > CHILD_LIMIT;
341
- descendants.slice(0, CHILD_LIMIT).forEach((element, index) => {
342
- if (isHidden(element)) {
343
- excluded.push({ index, tag: element.tagName.toLowerCase(), reason: "hidden" });
344
- return;
345
- }
346
- const rect = element.getBoundingClientRect();
347
- if (rect.width <= 0 || rect.height <= 0) {
348
- excluded.push({ index, tag: element.tagName.toLowerCase(), reason: "zero-area" });
349
- return;
372
+ let truncated = false;
373
+ let measurementIndex = 0;
374
+ const recordIssue = (element, allowed, clippedBy, axes = { x: true, y: true }) => {
375
+ const rect = element.rect;
376
+ const overrun = {
377
+ top: axes.y ? Math.max(0, allowed.top - rect.top) : 0,
378
+ right: axes.x ? Math.max(0, rect.right - allowed.right) : 0,
379
+ bottom: axes.y ? Math.max(0, rect.bottom - allowed.bottom) : 0,
380
+ left: axes.x ? Math.max(0, allowed.left - rect.left) : 0,
381
+ };
382
+ const maxOverrunPx = Math.max(overrun.top, overrun.right, overrun.bottom, overrun.left);
383
+ if (maxOverrunPx > tolerancePx + SUBPIXEL_EPSILON && issues.length < ISSUE_LIMIT) {
384
+ issues.push({ element, clippedBy, overrun, maxOverrunPx });
350
385
  }
351
- let allowed = paddingRect(container);
352
- let clippedBy = labelOf(container);
353
- let current = element.parentElement;
386
+ };
387
+ // Walk from `start` up to `container`, intersecting the allowed rect with
388
+ // every ancestor that genuinely clips. `free` records an axis the reader
389
+ // can scroll, after which nothing further constrains that axis -- the
390
+ // scroller's own box still has to fit its outer chain, but its contents
391
+ // are reachable and so are exempt.
392
+ const clipChain = (start, container, seed, seedLabel) => {
393
+ let allowed = { ...seed };
394
+ let clippedBy = seedLabel;
395
+ const free = { x: false, y: false };
396
+ let current = start;
354
397
  while (current && container.contains(current)) {
355
398
  const style = getComputedStyle(current);
356
399
  if (style.clipPath && style.clipPath !== "none")
357
400
  unsupported.add(`${labelOf(current)}:clip-path`);
358
- if (style.borderRadius && !/^0(?:px)?(?: 0(?:px)?){0,3}$/.test(style.borderRadius)) {
359
- unsupported.add(`${labelOf(current)}:border-radius`);
360
- }
361
401
  if (style.transform && style.transform !== "none")
362
402
  unsupported.add(`${labelOf(current)}:transform`);
363
- const clips = current === container ? { x: true, y: true } : clippingStyle(current);
403
+ // The container bounds by default even when it does not CSS-clip,
404
+ // since it is the scope the caller asked about -- but a scroller
405
+ // hands its axis back whether it is the scope or an ancestor.
406
+ const scope = current === container;
407
+ const scrolls = scrollingStyle(current);
408
+ const css = scope ? { x: true, y: true } : clippingStyle(current);
409
+ const clips = {
410
+ x: css.x && !free.x && !scrolls.x,
411
+ y: css.y && !free.y && !scrolls.y,
412
+ };
364
413
  if (clips.x || clips.y) {
365
414
  const candidate = paddingRect(current);
366
415
  allowed = {
@@ -377,28 +426,122 @@ export function buildLayoutLintCheck() {
377
426
  allowed.height = Math.max(0, allowed.bottom - allowed.top);
378
427
  clippedBy = labelOf(current);
379
428
  }
380
- if (current === container)
429
+ if (scrolls.x)
430
+ free.x = true;
431
+ if (scrolls.y)
432
+ free.y = true;
433
+ if (scope)
381
434
  break;
382
435
  current = current.parentElement;
383
436
  }
384
- const overrun = {
385
- top: Math.max(0, allowed.top - rect.top),
386
- right: Math.max(0, rect.right - allowed.right),
387
- bottom: Math.max(0, rect.bottom - allowed.bottom),
388
- left: Math.max(0, allowed.left - rect.left),
389
- };
390
- const maxOverrunPx = Math.max(overrun.top, overrun.right, overrun.bottom, overrun.left);
391
- if (maxOverrunPx > tolerancePx && issues.length < ISSUE_LIMIT) {
392
- issues.push({
393
- element: measure(element, index, false),
394
- clippedBy,
395
- overrun,
396
- maxOverrunPx,
397
- });
437
+ return { allowed, clippedBy };
438
+ };
439
+ const textContainer = (node, scope) => {
440
+ let current = node.parentElement;
441
+ while (current && scope.contains(current)) {
442
+ if (current instanceof SVGElement)
443
+ return null;
444
+ const display = getComputedStyle(current).display;
445
+ if (display !== "inline" && display !== "contents")
446
+ return current;
447
+ if (current === scope)
448
+ return current;
449
+ current = current.parentElement;
398
450
  }
399
- });
451
+ return scope;
452
+ };
453
+ for (const container of containers) {
454
+ const descendants = [...container.querySelectorAll("*")];
455
+ if (descendants.length > CHILD_LIMIT)
456
+ truncated = true;
457
+ descendants.slice(0, CHILD_LIMIT).forEach((element) => {
458
+ const index = measurementIndex++;
459
+ if (isHidden(element)) {
460
+ excluded.push({ index, tag: element.tagName.toLowerCase(), reason: "hidden" });
461
+ return;
462
+ }
463
+ const rect = element.getBoundingClientRect();
464
+ if (rect.width <= 0 || rect.height <= 0) {
465
+ excluded.push({ index, tag: element.tagName.toLowerCase(), reason: "zero-area" });
466
+ return;
467
+ }
468
+ const chain = clipChain(element.parentElement, container, unbounded(), labelOf(container));
469
+ recordIssue(measure(element, index, false), chain.allowed, chain.clippedBy);
470
+ });
471
+ const walker = document.createTreeWalker(container, NodeFilter.SHOW_TEXT);
472
+ let textCount = 0;
473
+ for (let node = walker.nextNode(); node; node = walker.nextNode()) {
474
+ if (textCount >= TEXT_LIMIT) {
475
+ truncated = true;
476
+ break;
477
+ }
478
+ const value = node.nodeValue ?? "";
479
+ if (!/\S/.test(value))
480
+ continue;
481
+ const parent = node.parentElement;
482
+ if (!parent ||
483
+ ["SCRIPT", "STYLE", "NOSCRIPT", "TEMPLATE"].includes(parent.tagName) ||
484
+ isHidden(parent)) {
485
+ continue;
486
+ }
487
+ const owner = textContainer(node, container);
488
+ if (!owner)
489
+ continue;
490
+ let transformed = false;
491
+ let current = owner;
492
+ while (current && container.contains(current)) {
493
+ const transform = getComputedStyle(current).transform;
494
+ if (transform && transform !== "none") {
495
+ unsupported.add(`${labelOf(current)}:transform`);
496
+ transformed = true;
497
+ break;
498
+ }
499
+ if (current === container)
500
+ break;
501
+ current = current.parentElement;
502
+ }
503
+ if (transformed)
504
+ continue;
505
+ textCount++;
506
+ // Text glyph boxes routinely extend a few pixels above/below their
507
+ // line box. That is normal font paint, not clipping. Constrain text
508
+ // horizontally to its nearest block owner, vertically to the scope,
509
+ // and on either axis where a CSS ancestor actually clips.
510
+ const seed = unbounded();
511
+ // Constrain text horizontally to its nearest block owner -- unless
512
+ // that owner scrolls sideways, where running past its edge is how a
513
+ // long line is meant to behave.
514
+ if (!scrollingStyle(owner).x) {
515
+ const ownerRect = paddingRect(owner);
516
+ seed.left = ownerRect.left;
517
+ seed.right = ownerRect.right;
518
+ seed.x = seed.left;
519
+ seed.width = Math.max(0, seed.right - seed.left);
520
+ }
521
+ const chain = clipChain(owner, container, seed, labelOf(owner));
522
+ const allowed = chain.allowed;
523
+ const clippedBy = chain.clippedBy;
524
+ const range = document.createRange();
525
+ range.selectNodeContents(node);
526
+ for (const fragment of [...range.getClientRects()]) {
527
+ if (fragment.width <= 0 || fragment.height <= 0)
528
+ continue;
529
+ const index = measurementIndex++;
530
+ recordIssue({
531
+ index,
532
+ tag: parent.tagName.toLowerCase(),
533
+ label: labelOf(parent),
534
+ snippet: value.replace(/\s+/g, " ").trim().slice(0, 160),
535
+ source: "text",
536
+ rect: rectOf(fragment),
537
+ }, allowed, clippedBy);
538
+ }
539
+ }
540
+ }
400
541
  if (issues.length >= ISSUE_LIMIT)
401
542
  truncated = true;
543
+ // Border radius keeps the rectangular padding-box contract; only shapes
544
+ // the rectangle cannot certify contribute to unsupported geometry.
402
545
  const outcome = issues.length > 0 ? "fail" : unsupported.size > 0 ? "unknown" : "pass";
403
546
  return {
404
547
  rule: "clip",
@@ -1,12 +1,16 @@
1
1
  export { type AssertOp, type AssertResult, type AssertSpec, buildAssertCheck, parseAssertSpec, } from "./asserts.js";
2
- export { Browser, type BrowserOptions, type ConsoleEvent, type Diagnostics, type FailedRequest, type NavigateResult, type PageErrorEvent, } from "./client.js";
2
+ export { Browser, type BrowserOptions, BrowserSessionActionError, type BrowserSessionControl, type BrowserSessionInspection, type BrowserSessionLocator, type BrowserSessionNavigation, type BrowserSessionNavigationType, type BrowserSessionScreenshot, type BrowserSessionStatus, type BrowserSessionTab, type ConsoleEvent, type Diagnostics, type FailedRequest, type NavigateResult, type PageErrorEvent, } from "./client.js";
3
3
  export type { CheckRect, ContentAnnotationBox, ContentChecksRequest, ContentChecksResult, ContrastHit, ContrastResult, ImageHealthResult, ImageHit, PlaceholderHit, PlaceholderResult, TruncationHit, TruncationResult, } from "./content-checks.js";
4
4
  export { bandRects, type CritiqueFinding, type CritiqueProvider, type CritiqueResult, type CritiqueTile, DEFAULT_CRITIQUE_RUBRIC, normalizeFindings, runCritique, tilesFromFullPage, } from "./critique.js";
5
5
  export { captureDevOverlay, type DevOverlayError, type DevOverlayResult, } from "./dev-overlay.js";
6
6
  export type { AlignChild, AlignCluster, AlignResult, ClipIssue, ClipResult, CrowdPair, CrowdResult, GapCluster, GapPair, GapResult, LayoutAxis, LayoutElementMeasurement, LayoutExclusion, LayoutLintRequest, LayoutLintResult, LayoutOutcome, LayoutRect, OverlapIssue, OverlapResult, } from "./geometry.js";
7
+ export { buildLayoutLintCheck } from "./geometry.js";
7
8
  export { isWSL, wslHeadedLaunchArgs } from "./launch-args.js";
8
9
  export type { OverflowElement, OverflowResult, WidthResult, } from "./layout.js";
10
+ export { serializeNetscapeCookies, writeNetscapeCookieFile, } from "./netscape-cookies.js";
11
+ export { type BrowserProxy, type BrowserProxyGate, browserProxyFromEnv, browserProxyGateFromEnv, extractObservedIp, WEBRTC_PROXY_ONLY_ARG, } from "./proxy.js";
9
12
  export type { RuntHit, RuntsResult, } from "./runts.js";
13
+ export { BROWSER_SESSION_MAX_FILL_BYTES, BROWSER_SESSION_MAX_FRAME_BYTES, BROWSER_SESSION_MAX_INSPECTION_TEXT_BYTES, BROWSER_SESSION_PROTOCOL_VERSION, type BrowserSessionDescriptor, BrowserSessionError, type BrowserSessionRequest, type BrowserSessionResponse, type BrowserSessionResult, type BrowserSessionServer, type BrowserSessionTarget, browserSessionTransport, readBrowserSessionDescriptor, sendBrowserSessionRequest, startBrowserSessionServer, } from "./session-control.js";
10
14
  export type { TargetSizeNode, TargetSizeOutcome, TargetSizeProfile, TargetSizeResult, } from "./target-size.js";
11
15
  export type { CheckVisibilityOptions, VisibilityResult, VisibilitySample, } from "./visibility.js";
12
16
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/lib/browser/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,KAAK,QAAQ,EACb,KAAK,YAAY,EACjB,KAAK,UAAU,EACf,gBAAgB,EAChB,eAAe,GAChB,MAAM,cAAc,CAAC;AACtB,OAAO,EACL,OAAO,EACP,KAAK,cAAc,EACnB,KAAK,YAAY,EACjB,KAAK,WAAW,EAChB,KAAK,aAAa,EAClB,KAAK,cAAc,EACnB,KAAK,cAAc,GACpB,MAAM,aAAa,CAAC;AACrB,YAAY,EACV,SAAS,EACT,oBAAoB,EACpB,oBAAoB,EACpB,mBAAmB,EACnB,WAAW,EACX,cAAc,EACd,iBAAiB,EACjB,QAAQ,EACR,cAAc,EACd,iBAAiB,EACjB,aAAa,EACb,gBAAgB,GACjB,MAAM,qBAAqB,CAAC;AAC7B,OAAO,EACL,SAAS,EACT,KAAK,eAAe,EACpB,KAAK,gBAAgB,EACrB,KAAK,cAAc,EACnB,KAAK,YAAY,EACjB,uBAAuB,EACvB,iBAAiB,EACjB,WAAW,EACX,iBAAiB,GAClB,MAAM,eAAe,CAAC;AACvB,OAAO,EACL,iBAAiB,EACjB,KAAK,eAAe,EACpB,KAAK,gBAAgB,GACtB,MAAM,kBAAkB,CAAC;AAC1B,YAAY,EACV,UAAU,EACV,YAAY,EACZ,WAAW,EACX,SAAS,EACT,UAAU,EACV,SAAS,EACT,WAAW,EACX,UAAU,EACV,OAAO,EACP,SAAS,EACT,UAAU,EACV,wBAAwB,EACxB,eAAe,EACf,iBAAiB,EACjB,gBAAgB,EAChB,aAAa,EACb,UAAU,EACV,YAAY,EACZ,aAAa,GACd,MAAM,eAAe,CAAC;AACvB,OAAO,EAAE,KAAK,EAAE,mBAAmB,EAAE,MAAM,kBAAkB,CAAC;AAC9D,YAAY,EACV,eAAe,EACf,cAAc,EACd,WAAW,GACZ,MAAM,aAAa,CAAC;AACrB,YAAY,EACV,OAAO,EACP,WAAW,GACZ,MAAM,YAAY,CAAC;AACpB,YAAY,EACV,cAAc,EACd,iBAAiB,EACjB,iBAAiB,EACjB,gBAAgB,GACjB,MAAM,kBAAkB,CAAC;AAC1B,YAAY,EACV,sBAAsB,EACtB,gBAAgB,EAChB,gBAAgB,GACjB,MAAM,iBAAiB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/lib/browser/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,KAAK,QAAQ,EACb,KAAK,YAAY,EACjB,KAAK,UAAU,EACf,gBAAgB,EAChB,eAAe,GAChB,MAAM,cAAc,CAAC;AACtB,OAAO,EACL,OAAO,EACP,KAAK,cAAc,EACnB,yBAAyB,EACzB,KAAK,qBAAqB,EAC1B,KAAK,wBAAwB,EAC7B,KAAK,qBAAqB,EAC1B,KAAK,wBAAwB,EAC7B,KAAK,4BAA4B,EACjC,KAAK,wBAAwB,EAC7B,KAAK,oBAAoB,EACzB,KAAK,iBAAiB,EACtB,KAAK,YAAY,EACjB,KAAK,WAAW,EAChB,KAAK,aAAa,EAClB,KAAK,cAAc,EACnB,KAAK,cAAc,GACpB,MAAM,aAAa,CAAC;AACrB,YAAY,EACV,SAAS,EACT,oBAAoB,EACpB,oBAAoB,EACpB,mBAAmB,EACnB,WAAW,EACX,cAAc,EACd,iBAAiB,EACjB,QAAQ,EACR,cAAc,EACd,iBAAiB,EACjB,aAAa,EACb,gBAAgB,GACjB,MAAM,qBAAqB,CAAC;AAC7B,OAAO,EACL,SAAS,EACT,KAAK,eAAe,EACpB,KAAK,gBAAgB,EACrB,KAAK,cAAc,EACnB,KAAK,YAAY,EACjB,uBAAuB,EACvB,iBAAiB,EACjB,WAAW,EACX,iBAAiB,GAClB,MAAM,eAAe,CAAC;AACvB,OAAO,EACL,iBAAiB,EACjB,KAAK,eAAe,EACpB,KAAK,gBAAgB,GACtB,MAAM,kBAAkB,CAAC;AAC1B,YAAY,EACV,UAAU,EACV,YAAY,EACZ,WAAW,EACX,SAAS,EACT,UAAU,EACV,SAAS,EACT,WAAW,EACX,UAAU,EACV,OAAO,EACP,SAAS,EACT,UAAU,EACV,wBAAwB,EACxB,eAAe,EACf,iBAAiB,EACjB,gBAAgB,EAChB,aAAa,EACb,UAAU,EACV,YAAY,EACZ,aAAa,GACd,MAAM,eAAe,CAAC;AACvB,OAAO,EAAE,oBAAoB,EAAE,MAAM,eAAe,CAAC;AACrD,OAAO,EAAE,KAAK,EAAE,mBAAmB,EAAE,MAAM,kBAAkB,CAAC;AAC9D,YAAY,EACV,eAAe,EACf,cAAc,EACd,WAAW,GACZ,MAAM,aAAa,CAAC;AACrB,OAAO,EACL,wBAAwB,EACxB,uBAAuB,GACxB,MAAM,uBAAuB,CAAC;AAC/B,OAAO,EACL,KAAK,YAAY,EACjB,KAAK,gBAAgB,EACrB,mBAAmB,EACnB,uBAAuB,EACvB,iBAAiB,EACjB,qBAAqB,GACtB,MAAM,YAAY,CAAC;AACpB,YAAY,EACV,OAAO,EACP,WAAW,GACZ,MAAM,YAAY,CAAC;AACpB,OAAO,EACL,8BAA8B,EAC9B,+BAA+B,EAC/B,yCAAyC,EACzC,gCAAgC,EAChC,KAAK,wBAAwB,EAC7B,mBAAmB,EACnB,KAAK,qBAAqB,EAC1B,KAAK,sBAAsB,EAC3B,KAAK,oBAAoB,EACzB,KAAK,oBAAoB,EACzB,KAAK,oBAAoB,EACzB,uBAAuB,EACvB,4BAA4B,EAC5B,yBAAyB,EACzB,yBAAyB,GAC1B,MAAM,sBAAsB,CAAC;AAC9B,YAAY,EACV,cAAc,EACd,iBAAiB,EACjB,iBAAiB,EACjB,gBAAgB,GACjB,MAAM,kBAAkB,CAAC;AAC1B,YAAY,EACV,sBAAsB,EACtB,gBAAgB,EAChB,gBAAgB,GACjB,MAAM,iBAAiB,CAAC"}
@@ -1,5 +1,9 @@
1
1
  export { buildAssertCheck, parseAssertSpec, } from "./asserts.js";
2
- export { Browser, } from "./client.js";
2
+ export { Browser, BrowserSessionActionError, } from "./client.js";
3
3
  export { bandRects, DEFAULT_CRITIQUE_RUBRIC, normalizeFindings, runCritique, tilesFromFullPage, } from "./critique.js";
4
4
  export { captureDevOverlay, } from "./dev-overlay.js";
5
+ export { buildLayoutLintCheck } from "./geometry.js";
5
6
  export { isWSL, wslHeadedLaunchArgs } from "./launch-args.js";
7
+ export { serializeNetscapeCookies, writeNetscapeCookieFile, } from "./netscape-cookies.js";
8
+ export { browserProxyFromEnv, browserProxyGateFromEnv, extractObservedIp, WEBRTC_PROXY_ONLY_ARG, } from "./proxy.js";
9
+ export { BROWSER_SESSION_MAX_FILL_BYTES, BROWSER_SESSION_MAX_FRAME_BYTES, BROWSER_SESSION_MAX_INSPECTION_TEXT_BYTES, BROWSER_SESSION_PROTOCOL_VERSION, BrowserSessionError, browserSessionTransport, readBrowserSessionDescriptor, sendBrowserSessionRequest, startBrowserSessionServer, } from "./session-control.js";
@@ -0,0 +1,6 @@
1
+ import type { Cookie } from "playwright";
2
+ /** Serialize Playwright cookies in the Netscape cookies.txt format used by yt-dlp. */
3
+ export declare function serializeNetscapeCookies(cookies: readonly Cookie[]): string;
4
+ /** Write a credential-bearing cookie file with owner-only permissions. */
5
+ export declare function writeNetscapeCookieFile(path: string, cookies: readonly Cookie[]): void;
6
+ //# sourceMappingURL=netscape-cookies.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"netscape-cookies.d.ts","sourceRoot":"","sources":["../../../src/lib/browser/netscape-cookies.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,YAAY,CAAC;AAMzC,sFAAsF;AACtF,wBAAgB,wBAAwB,CAAC,OAAO,EAAE,SAAS,MAAM,EAAE,GAAG,MAAM,CAsB3E;AAED,0EAA0E;AAC1E,wBAAgB,uBAAuB,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,SAAS,MAAM,EAAE,GAAG,IAAI,CAItF"}
@@ -0,0 +1,31 @@
1
+ import { chmodSync, mkdirSync, writeFileSync } from "node:fs";
2
+ import { dirname } from "node:path";
3
+ function cookieField(value) {
4
+ return value.replace(/[\t\r\n]/g, "");
5
+ }
6
+ /** Serialize Playwright cookies in the Netscape cookies.txt format used by yt-dlp. */
7
+ export function serializeNetscapeCookies(cookies) {
8
+ const lines = ["# Netscape HTTP Cookie File", "# Generated by harn browse. Keep private.", ""];
9
+ for (const cookie of [...cookies].sort((a, b) => `${a.domain}\0${a.path}\0${a.name}`.localeCompare(`${b.domain}\0${b.path}\0${b.name}`))) {
10
+ const domain = `${cookie.httpOnly ? "#HttpOnly_" : ""}${cookieField(cookie.domain)}`;
11
+ const includeSubdomains = cookie.domain.startsWith(".") ? "TRUE" : "FALSE";
12
+ const secure = cookie.secure ? "TRUE" : "FALSE";
13
+ const expires = cookie.expires > 0 ? String(Math.floor(cookie.expires)) : "0";
14
+ lines.push([
15
+ domain,
16
+ includeSubdomains,
17
+ cookieField(cookie.path || "/"),
18
+ secure,
19
+ expires,
20
+ cookieField(cookie.name),
21
+ cookieField(cookie.value),
22
+ ].join("\t"));
23
+ }
24
+ return `${lines.join("\n")}\n`;
25
+ }
26
+ /** Write a credential-bearing cookie file with owner-only permissions. */
27
+ export function writeNetscapeCookieFile(path, cookies) {
28
+ mkdirSync(dirname(path), { recursive: true });
29
+ writeFileSync(path, serializeNetscapeCookies(cookies), { mode: 0o600 });
30
+ chmodSync(path, 0o600);
31
+ }
@@ -0,0 +1,24 @@
1
+ export interface BrowserProxy {
2
+ server: string;
3
+ username?: string;
4
+ password?: string;
5
+ }
6
+ export interface BrowserProxyGate {
7
+ checkUrl: string;
8
+ expectedIp: string;
9
+ }
10
+ /**
11
+ * Parse an authenticated HTTP(S) proxy from the child environment without
12
+ * requiring credentials in command arguments. The returned server never
13
+ * contains userinfo, so Playwright owns the 407 challenge response.
14
+ */
15
+ export declare function browserProxyFromEnv(env?: NodeJS.ProcessEnv): BrowserProxy;
16
+ /**
17
+ * Read the optional fixed-egress gate injected by an embedding host wrapper.
18
+ * Both fields are required together so a partial contract
19
+ * fails before Chromium visits the requested target.
20
+ */
21
+ export declare function browserProxyGateFromEnv(env?: NodeJS.ProcessEnv): BrowserProxyGate | null;
22
+ export declare function extractObservedIp(body: string): string | null;
23
+ export declare const WEBRTC_PROXY_ONLY_ARG = "--force-webrtc-ip-handling-policy=disable_non_proxied_udp";
24
+ //# sourceMappingURL=proxy.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"proxy.d.ts","sourceRoot":"","sources":["../../../src/lib/browser/proxy.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,YAAY;IAC3B,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,EAAE,MAAM,CAAC;IACjB,UAAU,EAAE,MAAM,CAAC;CACpB;AAID;;;;GAIG;AACH,wBAAgB,mBAAmB,CAAC,GAAG,GAAE,MAAM,CAAC,UAAwB,GAAG,YAAY,CAkCtF;AAED;;;;GAIG;AACH,wBAAgB,uBAAuB,CACrC,GAAG,GAAE,MAAM,CAAC,UAAwB,GACnC,gBAAgB,GAAG,IAAI,CAsBzB;AAED,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAiB7D;AAED,eAAO,MAAM,qBAAqB,8DAA8D,CAAC"}
@@ -0,0 +1,84 @@
1
+ const PROXY_ENV_KEYS = ["HTTPS_PROXY", "https_proxy", "HTTP_PROXY", "http_proxy"];
2
+ /**
3
+ * Parse an authenticated HTTP(S) proxy from the child environment without
4
+ * requiring credentials in command arguments. The returned server never
5
+ * contains userinfo, so Playwright owns the 407 challenge response.
6
+ */
7
+ export function browserProxyFromEnv(env = process.env) {
8
+ const raw = PROXY_ENV_KEYS.map((key) => env[key]).find((value) => typeof value === "string" && value.trim().length > 0);
9
+ if (!raw) {
10
+ throw new Error("--proxy-from-env requires HTTPS_PROXY, https_proxy, HTTP_PROXY, or http_proxy.");
11
+ }
12
+ let url;
13
+ try {
14
+ url = new URL(raw);
15
+ }
16
+ catch {
17
+ throw new Error("The proxy environment contains an invalid URL.");
18
+ }
19
+ if (url.protocol !== "http:" && url.protocol !== "https:") {
20
+ throw new Error("Browser proxy URLs must use http:// or https://.");
21
+ }
22
+ if (!url.hostname || !url.port || url.pathname !== "/" || url.search || url.hash) {
23
+ throw new Error("Browser proxy URLs must contain only scheme, host, port, and optional credentials.");
24
+ }
25
+ if ((url.username && !url.password) || (!url.username && url.password)) {
26
+ throw new Error("Browser proxy URLs must provide both username and password, or neither.");
27
+ }
28
+ const proxy = { server: `${url.protocol}//${url.host}` };
29
+ if (url.username) {
30
+ proxy.username = decodeURIComponent(url.username);
31
+ proxy.password = decodeURIComponent(url.password);
32
+ }
33
+ return proxy;
34
+ }
35
+ /**
36
+ * Read the optional fixed-egress gate injected by an embedding host wrapper.
37
+ * Both fields are required together so a partial contract
38
+ * fails before Chromium visits the requested target.
39
+ */
40
+ export function browserProxyGateFromEnv(env = process.env) {
41
+ const expectedIp = env.HARNERY_BROWSER_PROXY_EXPECTED_IP?.trim();
42
+ const checkUrl = env.HARNERY_BROWSER_PROXY_CHECK_URL?.trim();
43
+ if (!expectedIp && !checkUrl)
44
+ return null;
45
+ if (!expectedIp || !checkUrl) {
46
+ throw new Error("HARNERY_BROWSER_PROXY_EXPECTED_IP and HARNERY_BROWSER_PROXY_CHECK_URL must be set together.");
47
+ }
48
+ if (!/^(?:\d{1,3}\.){3}\d{1,3}$/.test(expectedIp)) {
49
+ throw new Error("HARNERY_BROWSER_PROXY_EXPECTED_IP must be an IPv4 address.");
50
+ }
51
+ let parsed;
52
+ try {
53
+ parsed = new URL(checkUrl);
54
+ }
55
+ catch {
56
+ throw new Error("HARNERY_BROWSER_PROXY_CHECK_URL must be a valid HTTPS URL.");
57
+ }
58
+ if (parsed.protocol !== "https:") {
59
+ throw new Error("HARNERY_BROWSER_PROXY_CHECK_URL must use HTTPS.");
60
+ }
61
+ return { expectedIp, checkUrl: parsed.toString() };
62
+ }
63
+ export function extractObservedIp(body) {
64
+ try {
65
+ const parsed = JSON.parse(body);
66
+ for (const key of ["ip", "query", "origin"]) {
67
+ const value = parsed[key];
68
+ if (typeof value === "string" && value.trim())
69
+ return value.trim().split(",")[0].trim();
70
+ }
71
+ const proxy = parsed.proxy;
72
+ if (proxy && typeof proxy === "object") {
73
+ const value = proxy.ip;
74
+ if (typeof value === "string" && value.trim())
75
+ return value.trim();
76
+ }
77
+ }
78
+ catch {
79
+ // A plain-text IP endpoint is also valid.
80
+ }
81
+ const trimmed = body.trim();
82
+ return /^(?:\d{1,3}\.){3}\d{1,3}$/.test(trimmed) ? trimmed : null;
83
+ }
84
+ export const WEBRTC_PROXY_ONLY_ARG = "--force-webrtc-ip-handling-policy=disable_non_proxied_udp";
@@ -16,6 +16,12 @@ export interface RuntHit {
16
16
  lines: number;
17
17
  }
18
18
  export interface RuntsResult {
19
+ /** Requested selector scope, or null for the document body. */
20
+ scope: string | null;
21
+ /** False when a requested selector matched nothing. */
22
+ found: boolean;
23
+ /** Unknown means the sweep hit a safety cap before proving the page clean. */
24
+ outcome: "pass" | "fail" | "unknown";
19
25
  /** Text blocks scanned (post length/inline-children filters). */
20
26
  scannedBlocks: number;
21
27
  /** True when the block sweep hit the internal cap (very large page). */
@@ -1 +1 @@
1
- {"version":3,"file":"runts.d.ts","sourceRoot":"","sources":["../../../src/lib/browser/runts.ts"],"names":[],"mappings":"AAmBA,MAAM,WAAW,OAAO;IACtB,wEAAwE;IACxE,KAAK,EAAE,MAAM,CAAC;IACd,6EAA6E;IAC7E,IAAI,EAAE,MAAM,CAAC;IACb,4DAA4D;IAC5D,OAAO,EAAE,MAAM,CAAC;IAChB,wEAAwE;IACxE,IAAI,EAAE;QAAE,CAAC,EAAE,MAAM,CAAC;QAAC,CAAC,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,CAAC;IAC9D,2CAA2C;IAC3C,KAAK,EAAE,MAAM,CAAC;CACf;AAED,MAAM,WAAW,WAAW;IAC1B,iEAAiE;IACjE,aAAa,EAAE,MAAM,CAAC;IACtB,wEAAwE;IACxE,SAAS,EAAE,OAAO,CAAC;IACnB,KAAK,EAAE,OAAO,EAAE,CAAC;CAClB;AAED;;;;GAIG;AACH,wBAAgB,eAAe,IAAI,CAAC,IAAI,EAAE;IACxC,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,QAAQ,EAAE,MAAM,CAAC;CAClB,KAAK,WAAW,CAkHhB;AAED;;;;;GAKG;AACH,wBAAgB,wBAAwB,IAAI,CAAC,IAAI,EAAE;IAAE,KAAK,EAAE,OAAO,EAAE,CAAA;CAAE,KAAK,IAAI,CAyC/E;AAED,wBAAgB,gCAAgC,IAAI,MAAM,IAAI,CAI7D"}
1
+ {"version":3,"file":"runts.d.ts","sourceRoot":"","sources":["../../../src/lib/browser/runts.ts"],"names":[],"mappings":"AAmBA,MAAM,WAAW,OAAO;IACtB,wEAAwE;IACxE,KAAK,EAAE,MAAM,CAAC;IACd,6EAA6E;IAC7E,IAAI,EAAE,MAAM,CAAC;IACb,4DAA4D;IAC5D,OAAO,EAAE,MAAM,CAAC;IAChB,wEAAwE;IACxE,IAAI,EAAE;QAAE,CAAC,EAAE,MAAM,CAAC;QAAC,CAAC,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,CAAC;IAC9D,2CAA2C;IAC3C,KAAK,EAAE,MAAM,CAAC;CACf;AAED,MAAM,WAAW,WAAW;IAC1B,+DAA+D;IAC/D,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,uDAAuD;IACvD,KAAK,EAAE,OAAO,CAAC;IACf,8EAA8E;IAC9E,OAAO,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAAC;IACrC,iEAAiE;IACjE,aAAa,EAAE,MAAM,CAAC;IACtB,wEAAwE;IACxE,SAAS,EAAE,OAAO,CAAC;IACnB,KAAK,EAAE,OAAO,EAAE,CAAC;CAClB;AAED;;;;GAIG;AACH,wBAAgB,eAAe,IAAI,CAAC,IAAI,EAAE;IACxC,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,QAAQ,EAAE,MAAM,CAAC;CAClB,KAAK,WAAW,CAoIhB;AAED;;;;;GAKG;AACH,wBAAgB,wBAAwB,IAAI,CAAC,IAAI,EAAE;IAAE,KAAK,EAAE,OAAO,EAAE,CAAA;CAAE,KAAK,IAAI,CAyC/E;AAED,wBAAgB,gCAAgC,IAAI,MAAM,IAAI,CAI7D"}
@@ -45,8 +45,16 @@ export function buildRuntsCheck() {
45
45
  const BLOCK_CAP = 5000;
46
46
  const RUNT_CAP = 50;
47
47
  const root = scope ? document.querySelector(scope) : document.body;
48
- if (!root)
49
- return { scannedBlocks: 0, truncated: false, runts: [] };
48
+ if (!root) {
49
+ return {
50
+ scope,
51
+ found: false,
52
+ outcome: "fail",
53
+ scannedBlocks: 0,
54
+ truncated: false,
55
+ runts: [],
56
+ };
57
+ }
50
58
  const sx = window.scrollX;
51
59
  const sy = window.scrollY;
52
60
  const label = (el) => {
@@ -58,7 +66,9 @@ export function buildRuntsCheck() {
58
66
  return `${tag}.${cls.split(/\s+/).slice(0, 2).join(".")}`;
59
67
  return tag;
60
68
  };
61
- const all = root.querySelectorAll("*");
69
+ // Include the scope itself. A caller commonly points directly at a <p>,
70
+ // caption, or blurb; querySelectorAll("*") would otherwise scan nothing.
71
+ const all = [root, ...root.querySelectorAll("*")];
62
72
  const runts = [];
63
73
  let scanned = 0;
64
74
  let truncated = false;
@@ -136,7 +146,14 @@ export function buildRuntsCheck() {
136
146
  break;
137
147
  }
138
148
  }
139
- return { scannedBlocks: scanned, truncated, runts };
149
+ return {
150
+ scope,
151
+ found: true,
152
+ outcome: runts.length > 0 ? "fail" : truncated ? "unknown" : "pass",
153
+ scannedBlocks: scanned,
154
+ truncated,
155
+ runts,
156
+ };
140
157
  };
141
158
  }
142
159
  /**