@crouton-kit/capture 1.3.8 → 1.3.9

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 (136) hide show
  1. package/bin/capture +29841 -8539
  2. package/package.json +6 -3
  3. package/src/capture.ts +86 -173
  4. package/src/cdp/a11y.ts +113 -14
  5. package/src/cdp/args.ts +300 -205
  6. package/src/cdp/bridge/protocol.ts +93 -6
  7. package/src/cdp/bridge/server.ts +118 -53
  8. package/src/cdp/bridge/spawn.ts +53 -6
  9. package/src/cdp/client.ts +13 -3
  10. package/src/cdp/commands/bridge-serve.ts +20 -14
  11. package/src/cdp/commands/cdp.ts +259 -90
  12. package/src/cdp/commands/gate-guard.ts +43 -0
  13. package/src/cdp/commands/lib.ts +339 -213
  14. package/src/cdp/commands/measure/census.ts +76 -20
  15. package/src/cdp/commands/measure/check.ts +100 -26
  16. package/src/cdp/commands/measure/diff.ts +109 -21
  17. package/src/cdp/commands/measure/explain.ts +121 -19
  18. package/src/cdp/commands/measure/index.ts +31 -39
  19. package/src/cdp/commands/measure/map-ax.ts +52 -0
  20. package/src/cdp/commands/measure/map-focus.ts +41 -13
  21. package/src/cdp/commands/measure/map-layers.ts +49 -15
  22. package/src/cdp/commands/measure/map-scroll.ts +85 -15
  23. package/src/cdp/commands/measure/snap.ts +405 -25
  24. package/src/cdp/commands/measure/sweep.ts +217 -20
  25. package/src/cdp/commands/motion/index.ts +18 -17
  26. package/src/cdp/commands/motion/jank.ts +119 -9
  27. package/src/cdp/commands/motion/mask.ts +81 -12
  28. package/src/cdp/commands/motion/rec.ts +520 -19
  29. package/src/cdp/commands/motion/response.ts +65 -15
  30. package/src/cdp/commands/motion/timeline.ts +139 -14
  31. package/src/cdp/commands/page/click.ts +194 -0
  32. package/src/cdp/commands/page/elements.ts +154 -0
  33. package/src/cdp/commands/page/exec.ts +317 -0
  34. package/src/cdp/commands/page/index.ts +72 -0
  35. package/src/cdp/commands/page/navigate.ts +330 -0
  36. package/src/cdp/commands/page/scroll.ts +107 -0
  37. package/src/cdp/commands/page/shot.ts +208 -0
  38. package/src/cdp/commands/page/type.ts +104 -0
  39. package/src/cdp/commands/tab/index.ts +59 -0
  40. package/src/cdp/commands/tab/list.ts +167 -0
  41. package/src/cdp/commands/tab/network.ts +162 -0
  42. package/src/cdp/commands/tab/open.ts +90 -0
  43. package/src/cdp/commands/tab/reset.ts +160 -0
  44. package/src/cdp/connection.ts +368 -37
  45. package/src/cdp/detect.ts +42 -13
  46. package/src/cdp/dispatch.ts +74 -28
  47. package/src/cdp/har-recorder.ts +648 -115
  48. package/src/cdp/leaf-grammar.ts +99 -0
  49. package/src/cdp/measure/census.ts +205 -0
  50. package/src/cdp/measure/check.ts +318 -0
  51. package/src/cdp/measure/collectors/animation.ts +385 -0
  52. package/src/cdp/measure/collectors/ax.ts +278 -0
  53. package/src/cdp/measure/collectors/focus.ts +1041 -0
  54. package/src/cdp/measure/collectors/forms.ts +905 -0
  55. package/src/cdp/measure/collectors/geometry.ts +1038 -0
  56. package/src/cdp/measure/collectors/hittest.ts +835 -0
  57. package/src/cdp/measure/collectors/layers.ts +697 -0
  58. package/src/cdp/measure/collectors/media.ts +522 -0
  59. package/src/cdp/measure/collectors/pixels.ts +1584 -0
  60. package/src/cdp/measure/collectors/queries.ts +464 -0
  61. package/src/cdp/measure/collectors/scroll.ts +599 -0
  62. package/src/cdp/measure/collectors/states.ts +992 -0
  63. package/src/cdp/measure/collectors/style-provenance.ts +643 -0
  64. package/src/cdp/measure/collectors/styles.ts +355 -0
  65. package/src/cdp/measure/collectors/text.ts +713 -0
  66. package/src/cdp/measure/diff.ts +470 -0
  67. package/src/cdp/measure/explain.ts +498 -0
  68. package/src/cdp/measure/map-ax.ts +633 -0
  69. package/src/cdp/measure/map-focus.ts +188 -0
  70. package/src/cdp/measure/map-layers.ts +177 -0
  71. package/src/cdp/measure/map-scroll.ts +317 -0
  72. package/src/cdp/measure/redaction.ts +51 -0
  73. package/src/cdp/measure/settle.ts +1051 -0
  74. package/src/cdp/measure/snapshot.ts +390 -0
  75. package/src/cdp/measure/sweep.ts +330 -0
  76. package/src/cdp/measure/types.ts +220 -0
  77. package/src/cdp/measure/v2/collectors.ts +19 -0
  78. package/src/cdp/measure/v2/orchestrator.ts +46 -0
  79. package/src/cdp/measure/v2/snap-selection.ts +33 -0
  80. package/src/cdp/measure/v2/target-lock.ts +15 -0
  81. package/src/cdp/measure/v2/target.ts +16 -0
  82. package/src/cdp/motion/jank.ts +433 -0
  83. package/src/cdp/motion/mask.ts +421 -0
  84. package/src/cdp/motion/recorder.ts +764 -0
  85. package/src/cdp/motion/response.ts +356 -0
  86. package/src/cdp/motion/timeline.ts +215 -0
  87. package/src/cdp/record.ts +5 -212
  88. package/src/cdp/recorder-bridge.ts +1478 -105
  89. package/src/cdp/recorder-client.ts +326 -0
  90. package/src/cdp/scope-lock.ts +73 -0
  91. package/src/cdp/screenshot.ts +199 -66
  92. package/src/cdp/targets.ts +21 -1
  93. package/src/cdp/timing.ts +15 -10
  94. package/src/cdp/types.ts +22 -7
  95. package/src/cdp/viewport.ts +21 -0
  96. package/src/cdp.ts +12 -58
  97. package/src/command-help.ts +29 -0
  98. package/src/command-resolution.ts +22 -0
  99. package/src/command-walker.ts +35 -0
  100. package/src/contracts/a11y.ts +33 -0
  101. package/src/contracts/cursor.ts +35 -0
  102. package/src/contracts/index.ts +15 -0
  103. package/src/contracts/motion.ts +19 -0
  104. package/src/contracts/neutral.ts +56 -0
  105. package/src/contracts/primitives.ts +205 -0
  106. package/src/contracts/release.ts +17 -0
  107. package/src/contracts/results.ts +208 -0
  108. package/src/contracts/routes.ts +140 -0
  109. package/src/contracts/snapshot.ts +92 -0
  110. package/src/cursor/index.ts +13 -0
  111. package/src/cursor/service.ts +310 -0
  112. package/src/errors.ts +71 -0
  113. package/src/har-manager.ts +992 -41
  114. package/src/interact.ts +410 -98
  115. package/src/output/artifact-lifecycle.ts +371 -0
  116. package/src/output/artifact.ts +6 -0
  117. package/src/output/exact-raw.ts +114 -0
  118. package/src/output/render.ts +396 -2
  119. package/src/output/structured.ts +211 -0
  120. package/src/registry.ts +208 -0
  121. package/src/session/artifacts.ts +452 -200
  122. package/src/session/commands.ts +1162 -397
  123. package/src/session/coordinator.ts +251 -0
  124. package/src/session/har-filter.ts +57 -0
  125. package/src/session/log-tailer.ts +653 -0
  126. package/src/session-context.ts +279 -51
  127. package/src/vault/bundle.ts +32 -17
  128. package/src/cdp/commands/exec.ts +0 -105
  129. package/src/cdp/commands/har.ts +0 -128
  130. package/src/cdp/commands/tabs.ts +0 -183
  131. package/src/cdp/commands/traffic.ts +0 -121
  132. package/src/cdp/commands/ui.ts +0 -172
  133. package/src/cdp/execute.ts +0 -113
  134. package/src/cdp/har-output.ts +0 -47
  135. package/src/cdp/usage.ts +0 -79
  136. package/src/tab-session.ts +0 -91
@@ -7,6 +7,7 @@
7
7
  * command does — leaf commands never need to know they're nested under a
8
8
  * branch.
9
9
  */
10
+ import { invalidInput } from '../../../errors.js';
10
11
  import { type ParsedArgs } from '../../types.js';
11
12
  import { cmdMeasureSnap } from './snap.js';
12
13
  import { cmdMeasureCheck } from './check.js';
@@ -17,53 +18,44 @@ import { cmdMeasureSweep } from './sweep.js';
17
18
  import { cmdMeasureMapFocus } from './map-focus.js';
18
19
  import { cmdMeasureMapScroll } from './map-scroll.js';
19
20
  import { cmdMeasureMapLayers } from './map-layers.js';
21
+ import { cmdMeasureMapAx } from './map-ax.js';
22
+
23
+ /** Root-help representation of this branch, assembled by `src/capture.ts`. */
24
+ export const COMMAND_BLOCK = `<command name="measure">
25
+ static facts over a settled snapshot — \`snap\` writes the substrate, every other leaf is a read-only query over it
26
+ use when measuring layout/content/targetability facts, diffing snapshots, or reading one facet (focus, scroll, layers, ax) of the substrate
27
+ snap · check · diff · census · explain · sweep · map — \`capture measure -h\`
28
+ </command>`;
20
29
 
21
30
  export const MEASURE_USAGE = `capture measure — enriched snapshot substrate + read-only queries over it.
22
31
 
23
32
  \`snap\` drives the page (or a base snapshot) and writes one settled artifact
24
33
  directory; every other leaf below is a cheap read over that artifact and
25
- never re-drives the browser unless it explicitly accepts a URL target.
26
-
27
- Leaves:
28
- snap [url|snap] Drive + write a settled snapshot substrate
29
- [--freeze-animations] [--settle-timeout <ms>] [--capture-unsettled]
30
- [--pixels] [--state <state[:selector]>]...
31
-
32
- check [url|snap] [--for <checks>] [--viewport <WxH>]... [--gate]
33
- Threshold/fact checks over a snapshot
34
-
35
- diff --before <snap> --after <snap> [--pixels] [--full] [--gate]
36
- Structured before/after snapshot diff
37
-
38
- census [--snap <id>]... [--url <url>]... [--set-file <path>] --axis <axis>
39
- Value distributions across one or more snapshots
40
- (--snap/--url repeatable)
34
+ never re-drives the browser unless its target is a URL (which snaps first).
35
+ Every leaf defaults to rendered prose; --json mirrors the same result.
36
+ Findings exit 0 — a report, not a failure.
37
+ \`--gate\` (exit 2 on findings/changes) is accepted only by check and diff.
41
38
 
42
- explain <snap> --selector <sel> [--size] [--text] [--form]
43
- Per-element cascade/stacking/clipping/size/text/form explanation
39
+ <subcommand name="snap" args="[url|snap] [--freeze-animations] [--settle-timeout <ms>] [--capture-unsettled] [--pixels] [--state <state[:selector]>]... [--viewport <WxH>]..." whenToUse="drive + write the settled snapshot substrate every other leaf reads"/>
40
+ <subcommand name="check" args="[url|snap] [--for <checks>] [--gate]" whenToUse="read threshold/fact measurements from one snapshot"/>
41
+ <subcommand name="diff" args="--before <snap> --after <snap> [--pixels] [--full] [--gate]" whenToUse="structured before/after delta between two snapshots"/>
42
+ <subcommand name="census" args="[--snap <id>]... [--url <url>]... [--set-file <path>] --axis <axis>" whenToUse="value distributions across one or more snapshots"/>
43
+ <subcommand name="explain" args="<snap> --selector <sel> [--size] [--text] [--form]" whenToUse="per-element cascade/stacking/clipping/size/text/form explanation"/>
44
+ <subcommand name="sweep" args="[url] --axis <axis> [--from <val>] [--to <val>] [--viewport-height <val>]" whenToUse="responsive/environment sampling across an axis"/>
45
+ <subcommand name="map" args="focus|scroll|layers|ax [url|snap]" whenToUse="read one facet of a snapshot's substrate — capture measure map -h"/>
44
46
 
45
- sweep [url] --axis <axis> [--from <val>] [--to <val>] [--viewport-height <val>]
46
- Responsive/environment sampling
47
-
48
- map focus|scroll|layers [url|snap] Read one facet of a snapshot's substrate
49
- (see \`capture measure map --help\`)
50
-
51
- Every leaf defaults to rendered prose; --json mirrors the same result as JSON.
52
- Exit codes: 0 by default — findings are a report, not a failure.
53
- --gate exits 2 on findings/changes; only \`check\` and \`diff\` accept it.
54
-
55
- capture measure <leaf> --help Per-leaf usage`;
47
+ capture measure <leaf> -h Per-leaf usage`;
56
48
 
57
49
  export const MEASURE_MAP_USAGE = `capture measure map — read one facet of a snapshot's substrate (no browser re-drive).
58
50
 
59
- Leaves:
60
- focus [url|snap] Keyboard traversal order (focus.json)
61
- scroll [url|snap] Scroll-container topology (scroll.json)
62
- layers [url|snap] Paint/compositor layer map (layers.json)
63
-
64
51
  A URL target first creates a snap; a snap target reads its existing artifact.
65
52
 
66
- capture measure map <leaf> --help Per-leaf usage`;
53
+ <subcommand name="focus" args="[url|snap]" whenToUse="keyboard traversal order (focus.json)"/>
54
+ <subcommand name="scroll" args="[url|snap]" whenToUse="scroll-container topology (scroll.json)"/>
55
+ <subcommand name="layers" args="[url|snap]" whenToUse="paint/compositor layer map (layers.json)"/>
56
+ <subcommand name="ax" args="[url|snap]" whenToUse="AX-tree ↔ layout map (ax.json + geometry.json)"/>
57
+
58
+ capture measure map <leaf> -h Per-leaf usage`;
67
59
 
68
60
  export async function measureMain(parsed: ParsedArgs, args: string[]): Promise<void> {
69
61
  const leaf = parsed.positional[0];
@@ -88,8 +80,7 @@ export async function measureMain(parsed: ParsedArgs, args: string[]): Promise<v
88
80
  console.log(MEASURE_USAGE);
89
81
  return;
90
82
  default:
91
- console.error(`Unknown measure leaf: ${leaf}\n\n${MEASURE_USAGE}`);
92
- process.exit(1);
83
+ throw invalidInput(`Unknown measure leaf: ${leaf}.`, 'unknown_command');
93
84
  }
94
85
  }
95
86
 
@@ -104,11 +95,12 @@ async function measureMapMain(parsed: ParsedArgs, args: string[]): Promise<void>
104
95
  return cmdMeasureMapScroll(rest, args);
105
96
  case 'layers':
106
97
  return cmdMeasureMapLayers(rest, args);
98
+ case 'ax':
99
+ return cmdMeasureMapAx(rest, args);
107
100
  case undefined:
108
101
  console.log(MEASURE_MAP_USAGE);
109
102
  return;
110
103
  default:
111
- console.error(`Unknown measure map leaf: ${sub}\n\n${MEASURE_MAP_USAGE}`);
112
- process.exit(1);
104
+ throw invalidInput(`Unknown measure map leaf: ${sub}.`, 'unknown_command');
113
105
  }
114
106
  }
@@ -0,0 +1,52 @@
1
+ import { type ParsedArgs } from '../../types.js';
2
+ import { captureMeasureSnap } from './snap.js';
3
+ import { buildMeasureMapAxResult } from '../../measure/map-ax.js';
4
+ import { ArtifactResolutionError, resolveSnapRef } from '../../../output/artifact.js';
5
+ import { emitResult, fact, type RenderableResult } from '../../../output/render.js';
6
+
7
+ const USAGE = `capture measure map ax [url|snap] — accessibility-tree facts recorded in a snapshot's ax.json joined against geometry.json
8
+
9
+ input:
10
+ [url|snap] required target: a URL creates a settled snapshot first; a snapshot id or absolute path is read without re-driving the browser
11
+ output: <ax-map …> — non-ignored AX nodes with role, name, states, backendNodeId, and top-viewport rect; ignored AX nodes with their ignored-reasons; DOM elements with rendered boxes but no non-ignored AX node; and AX nodes whose rect is offscreen, clipped, or zero-size; --json mirrors
12
+ effects: read-only over an existing snapshot artifact; a URL target writes one settled snapshot first`;
13
+
14
+ function recoveryResult(err: unknown): RenderableResult {
15
+ const detail = err instanceof ArtifactResolutionError || err instanceof Error
16
+ ? err.message
17
+ : 'The snapshot artifacts could not be resolved.';
18
+ return {
19
+ tag: 'error',
20
+ attrs: { command: 'measure map ax', status: 'artifact_unavailable' },
21
+ summary: fact`AX-map facts could not be read: ${detail}`,
22
+ followUp: fact`Create a settled snapshot with \`capture measure snap <url>\`, then run \`capture measure map ax <snap>\`.`,
23
+ };
24
+ }
25
+
26
+ export async function cmdMeasureMapAx(parsed: ParsedArgs, _args: string[]): Promise<void> {
27
+ if (parsed.help) {
28
+ console.log(USAGE);
29
+ return;
30
+ }
31
+ if (parsed.positional.length !== 1) {
32
+ emitResult({
33
+ tag: 'error',
34
+ attrs: { command: 'measure map ax', status: 'invalid_target' },
35
+ summary: fact`Expected exactly one URL or snapshot target; received ${parsed.positional.length} positional target(s).`,
36
+ followUp: fact`Run \`capture measure map ax <snap>\` or \`capture measure map ax <url>\`.`,
37
+ }, { json: parsed.json });
38
+ process.exitCode = 1;
39
+ return;
40
+ }
41
+
42
+ try {
43
+ const target = parsed.positional[0]!;
44
+ const ref = await resolveSnapRef(target, {
45
+ onUrl: async (url) => captureMeasureSnap({ ...parsed, positional: [url] }, url),
46
+ });
47
+ emitResult(buildMeasureMapAxResult(ref), { json: parsed.json });
48
+ } catch (err) {
49
+ emitResult(recoveryResult(err), { json: parsed.json });
50
+ process.exitCode = 1;
51
+ }
52
+ }
@@ -1,24 +1,52 @@
1
1
  import { type ParsedArgs } from '../../types.js';
2
+ import { captureMeasureSnap } from './snap.js';
3
+ import { mapFocus } from '../../measure/map-focus.js';
4
+ import { ArtifactResolutionError, resolveSnapRef } from '../../../output/artifact.js';
2
5
  import { emitResult, fact, type RenderableResult } from '../../../output/render.js';
3
6
 
4
- const USAGE = `Usage: capture measure map focus [url|snap]
7
+ const USAGE = `capture measure map focus [url|snap] — keyboard traversal facts recorded in a snapshot's focus.json
5
8
 
6
- Keyboard traversal order recorded in a snapshot's focus.json: forward and
7
- reverse Tab sequence, rects, scroll jumps, focus-visible facts, and
8
- unreached focusable elements. A URL target creates a snap first.
9
+ input:
10
+ [url|snap] required target: a URL creates a settled snapshot first; a snapshot id or absolute path is read without re-driving the browser
11
+ output: <focus-map …> — forward and reverse Tab sequences, top-viewport rects, scroll jumps, focus-visible style facts, and unreached focusable elements; --json mirrors
12
+ effects: read-only over an existing snapshot artifact; a URL target writes one settled snapshot first`;
9
13
 
10
- Not yet implemented.`;
14
+ function recoveryResult(err: unknown): RenderableResult {
15
+ const detail = err instanceof ArtifactResolutionError || err instanceof Error
16
+ ? err.message
17
+ : 'The focus artifact could not be resolved.';
18
+ return {
19
+ tag: 'error',
20
+ attrs: { command: 'measure map focus', status: 'artifact_unavailable' },
21
+ summary: fact`Focus-map facts could not be read: ${detail}`,
22
+ followUp: fact`Create a settled snapshot with \`capture measure snap <url>\`, then run \`capture measure map focus <snap>\`.`,
23
+ };
24
+ }
11
25
 
12
26
  export async function cmdMeasureMapFocus(parsed: ParsedArgs, _args: string[]): Promise<void> {
13
27
  if (parsed.help) {
14
28
  console.log(USAGE);
15
- process.exit(0);
29
+ return;
30
+ }
31
+ if (parsed.positional.length !== 1) {
32
+ emitResult({
33
+ tag: 'error',
34
+ attrs: { command: 'measure map focus', status: 'invalid_target' },
35
+ summary: fact`Expected exactly one URL or snapshot target; received ${parsed.positional.length} positional target(s).`,
36
+ followUp: fact`Run \`capture measure map focus <snap>\` or \`capture measure map focus <url>\`.`,
37
+ }, { json: parsed.json });
38
+ process.exitCode = 1;
39
+ return;
40
+ }
41
+
42
+ try {
43
+ const target = parsed.positional[0]!;
44
+ const ref = await resolveSnapRef(target, {
45
+ onUrl: async (url) => captureMeasureSnap({ ...parsed, positional: [url] }, url),
46
+ });
47
+ emitResult(mapFocus(ref), { json: parsed.json });
48
+ } catch (err) {
49
+ emitResult(recoveryResult(err), { json: parsed.json });
50
+ process.exitCode = 1;
16
51
  }
17
- const result: RenderableResult = {
18
- tag: 'error',
19
- attrs: { command: 'measure map focus', status: 'not_implemented' },
20
- summary: fact`\`measure map focus\` is not implemented yet.`,
21
- };
22
- emitResult(result, { json: parsed.json });
23
- process.exit(1);
24
52
  }
@@ -1,25 +1,59 @@
1
1
  import { type ParsedArgs } from '../../types.js';
2
- import { emitResult, fact, type RenderableResult } from '../../../output/render.js';
2
+ import { captureMeasureSnap } from './snap.js';
3
+ import { buildMeasureMapLayersResult } from '../../measure/map-layers.js';
4
+ import { resolveSnapRef } from '../../../output/artifact.js';
5
+ import { emitResult, fact, text, type RenderableResult } from '../../../output/render.js';
3
6
 
4
- const USAGE = `Usage: capture measure map layers [url|snap]
7
+ const USAGE = `capture measure map layers [url|snap] — paint/compositor facts recorded in a snapshot's layers.json
5
8
 
6
- Paint/compositor layer map recorded in a snapshot's layers.json: layers,
7
- bounds, compositing reasons, paint order, per-node membership, and source
8
- provenance for layer-affecting declarations. A URL target creates a snap
9
- first.
9
+ input:
10
+ [url|snap] required target: a URL creates a settled snapshot first; a snapshot id or absolute path is read without re-driving the browser
11
+ output: <layer-map …> — layer bounds, compositing reasons, DOMSnapshot paint order, per-node membership, and available source provenance for layer-affecting declarations; --json mirrors
12
+ effects: read-only over an existing snapshot artifact; a URL target writes one settled snapshot first`;
10
13
 
11
- Not yet implemented.`;
14
+ function errorResult(err: unknown): RenderableResult {
15
+ const detail = err instanceof Error ? err.message : String(err);
16
+ return {
17
+ tag: 'error',
18
+ attrs: { command: 'measure map layers', status: 'artifact_unavailable' },
19
+ summary: fact`Layer-map facts could not be read: ${detail}`,
20
+ followUp: text`Pass a settled snapshot id or its absolute artifact path, or pass a URL to create a snapshot first with capture measure snap.`,
21
+ };
22
+ }
12
23
 
13
24
  export async function cmdMeasureMapLayers(parsed: ParsedArgs, _args: string[]): Promise<void> {
14
25
  if (parsed.help) {
15
26
  console.log(USAGE);
16
- process.exit(0);
27
+ return;
28
+ }
29
+ if (parsed.positional.length > 1) {
30
+ emitResult({
31
+ tag: 'error',
32
+ attrs: { command: 'measure map layers', status: 'invalid_target' },
33
+ summary: text`measure map layers accepts at most one URL, snapshot id, or absolute snapshot artifact path.`,
34
+ }, { json: parsed.json });
35
+ process.exitCode = 1;
36
+ return;
37
+ }
38
+ const target = parsed.positional[0];
39
+ if (!target) {
40
+ emitResult({
41
+ tag: 'error',
42
+ attrs: { command: 'measure map layers', status: 'missing_target' },
43
+ summary: text`A URL, snapshot id, or absolute snapshot artifact path is required to read layer-map facts.`,
44
+ followUp: text`Capture one with capture measure snap <url>, then pass its snapshot id or artifact path.`,
45
+ }, { json: parsed.json });
46
+ process.exitCode = 1;
47
+ return;
48
+ }
49
+
50
+ try {
51
+ const ref = await resolveSnapRef(target, {
52
+ onUrl: async (url) => captureMeasureSnap(parsed, url),
53
+ });
54
+ emitResult(buildMeasureMapLayersResult(ref), { json: parsed.json });
55
+ } catch (err) {
56
+ emitResult(errorResult(err), { json: parsed.json });
57
+ process.exitCode = 1;
17
58
  }
18
- const result: RenderableResult = {
19
- tag: 'error',
20
- attrs: { command: 'measure map layers', status: 'not_implemented' },
21
- summary: fact`\`measure map layers\` is not implemented yet.`,
22
- };
23
- emitResult(result, { json: parsed.json });
24
- process.exit(1);
25
59
  }
@@ -1,24 +1,94 @@
1
1
  import { type ParsedArgs } from '../../types.js';
2
- import { emitResult, fact, type RenderableResult } from '../../../output/render.js';
2
+ import { captureMeasureSnap } from './snap.js';
3
+ import { ArtifactResolutionError, resolveSnapRef, type SnapUrlResult } from '../../../output/artifact.js';
4
+ import { emitResult, fact, lineList, text, type FactLine, type RenderableResult } from '../../../output/render.js';
5
+ import { measureMapScroll } from '../../measure/map-scroll.js';
3
6
 
4
- const USAGE = `Usage: capture measure map scroll [url|snap]
7
+ const USAGE = `capture measure map scroll [url|snap] — scroll-container topology recorded in a snapshot's scroll.json
5
8
 
6
- Scroll-container topology recorded in a snapshot's scroll.json: containers,
7
- ranges, current/max offsets, sticky/fixed occupancy, snap points, and
8
- visual/layout viewport facts. A URL target creates a snap first.
9
+ input:
10
+ [url|snap] required target: a URL creates a settled snapshot first; a snapshot id from the active session or an absolute snapshot artifact path is read without re-driving the browser
11
+ output: <scroll-map …> — containers, ranges, current/max offsets, sticky/fixed occupancy, snap points, visual/layout viewport facts, and reachable-content samples; --json mirrors
12
+ effects: read-only over an existing snapshot artifact; a URL target writes one settled snapshot first`;
9
13
 
10
- Not yet implemented.`;
14
+ type CaptureSnap = (parsed: ParsedArgs, target: string) => Promise<SnapUrlResult>;
15
+ type Emit = typeof emitResult;
16
+
17
+ interface MapScrollDeps {
18
+ readonly captureSnap?: CaptureSnap;
19
+ readonly emit?: Emit;
20
+ }
21
+
22
+ function artifactRecovery(err: ArtifactResolutionError): FactLine[] {
23
+ return [
24
+ fact`artifact resolution error: ${err.message}`,
25
+ fact`received ref: ${err.ref}`,
26
+ fact`searched path count: ${err.searched.length}`,
27
+ ...err.searched.map((candidate) => fact`searched path: ${candidate}`),
28
+ fact`creating command: ${err.creatingCommand ?? 'capture measure snap'}`,
29
+ ];
30
+ }
31
+
32
+ export async function runMeasureMapScroll(parsed: ParsedArgs, deps: MapScrollDeps = {}): Promise<void> {
33
+ const captureSnap = deps.captureSnap ?? captureMeasureSnap;
34
+ const emit = deps.emit ?? emitResult;
11
35
 
12
- export async function cmdMeasureMapScroll(parsed: ParsedArgs, _args: string[]): Promise<void> {
13
36
  if (parsed.help) {
14
37
  console.log(USAGE);
15
- process.exit(0);
38
+ return;
39
+ }
40
+
41
+ if (parsed.positional.length > 1) {
42
+ const result: RenderableResult = {
43
+ tag: 'error',
44
+ attrs: { command: 'measure map scroll', status: 'invalid_target' },
45
+ summary: text`Measure map scroll accepts one URL, snapshot id, or absolute snapshot path.`,
46
+ followUp: text`Pass one target, for example: \`capture measure map scroll snap-a3f2\`.`,
47
+ };
48
+ emit(result, { json: parsed.json });
49
+ process.exitCode = 1;
50
+ return;
16
51
  }
17
- const result: RenderableResult = {
18
- tag: 'error',
19
- attrs: { command: 'measure map scroll', status: 'not_implemented' },
20
- summary: fact`\`measure map scroll\` is not implemented yet.`,
21
- };
22
- emitResult(result, { json: parsed.json });
23
- process.exit(1);
52
+
53
+ const target = parsed.positional[0] ?? parsed.url;
54
+ if (!target) {
55
+ const result: RenderableResult = {
56
+ tag: 'error',
57
+ attrs: { command: 'measure map scroll', status: 'missing_target' },
58
+ summary: text`No snapshot or URL target was received.`,
59
+ followUp: text`Create a snapshot with \`capture measure snap <url>\`, then pass its id or absolute path to \`capture measure map scroll\`.`,
60
+ };
61
+ emit(result, { json: parsed.json });
62
+ process.exitCode = 1;
63
+ return;
64
+ }
65
+
66
+ try {
67
+ const ref = await resolveSnapRef(target, {
68
+ onUrl: async (url) => captureSnap({ ...parsed, positional: [url] }, url),
69
+ });
70
+ emit(measureMapScroll(ref), { json: parsed.json });
71
+ } catch (err) {
72
+ const detail = err instanceof Error ? err.message : String(err);
73
+ const recovery = err instanceof ArtifactResolutionError ? artifactRecovery(err) : [];
74
+ const result: RenderableResult = {
75
+ tag: 'error',
76
+ attrs: {
77
+ command: 'measure map scroll',
78
+ status: 'artifact_unavailable',
79
+ recovery: err instanceof ArtifactResolutionError ? 'artifact-resolution-error' : undefined,
80
+ 'searched-paths': err instanceof ArtifactResolutionError ? err.searched.length : undefined,
81
+ 'creating-command': err instanceof ArtifactResolutionError ? (err.creatingCommand ?? 'capture measure snap') : undefined,
82
+ },
83
+ summary: fact`Scroll topology could not be read: ${detail}`,
84
+ sections: recovery.length ? [lineList(recovery)] : undefined,
85
+ followUp: text`Create a settled snapshot with \`capture measure snap <url>\`; it writes scroll.json for this query.`,
86
+ };
87
+ emit(result, { json: parsed.json });
88
+ process.exitCode = 1;
89
+ }
90
+ }
91
+
92
+ export async function cmdMeasureMapScroll(parsed: ParsedArgs, _args: string[]): Promise<void> {
93
+ await runMeasureMapScroll(parsed);
24
94
  }