taphound 0.2.0-dev.1

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 (176) hide show
  1. package/LICENSE +202 -0
  2. package/README.md +200 -0
  3. package/README.zh-CN.md +200 -0
  4. package/assets/brand/taphound-mark.svg +9 -0
  5. package/assets/skills/taphound-ai-journey/GUIDE.md +862 -0
  6. package/assets/skills/taphound-ai-journey/SKILL.md +377 -0
  7. package/assets/skills/taphound-ai-journey/prompts/analyze-project.md +526 -0
  8. package/assets/skills/taphound-ai-journey/prompts/check-completion.md +52 -0
  9. package/assets/skills/taphound-ai-journey/prompts/generate-step.md +109 -0
  10. package/assets/skills/taphound-ai-journey/schemas/observe-output.json +42 -0
  11. package/assets/skills/taphound-ai-journey/schemas/project-context.json +81 -0
  12. package/assets/skills/taphound-ai-journey/schemas/proposed-step-envelope.json +267 -0
  13. package/assets/skills/taphound-ai-journey/templates/project-context.example.json +49 -0
  14. package/dist/adapters/adb/adb-adapter.d.ts +23 -0
  15. package/dist/adapters/adb/adb-adapter.js +218 -0
  16. package/dist/adapters/android-cli/android-cli-adapter.d.ts +11 -0
  17. package/dist/adapters/android-cli/android-cli-adapter.js +64 -0
  18. package/dist/adapters/android-cli/layout-parser.d.ts +3 -0
  19. package/dist/adapters/android-cli/layout-parser.js +174 -0
  20. package/dist/adapters/clock/system-clock.d.ts +5 -0
  21. package/dist/adapters/clock/system-clock.js +13 -0
  22. package/dist/adapters/filesystem/artifact-store.d.ts +4 -0
  23. package/dist/adapters/filesystem/artifact-store.js +73 -0
  24. package/dist/adapters/filesystem/generation-meta-writer.d.ts +16 -0
  25. package/dist/adapters/filesystem/generation-meta-writer.js +34 -0
  26. package/dist/adapters/filesystem/generation-session-store.d.ts +68 -0
  27. package/dist/adapters/filesystem/generation-session-store.js +1603 -0
  28. package/dist/adapters/filesystem/journey-writer.d.ts +16 -0
  29. package/dist/adapters/filesystem/journey-writer.js +34 -0
  30. package/dist/adapters/filesystem/project-bound-file.d.ts +6 -0
  31. package/dist/adapters/filesystem/project-bound-file.js +240 -0
  32. package/dist/adapters/filesystem/project-file-inspector.d.ts +8 -0
  33. package/dist/adapters/filesystem/project-file-inspector.js +198 -0
  34. package/dist/adapters/filesystem/skill-installer.d.ts +10 -0
  35. package/dist/adapters/filesystem/skill-installer.js +39 -0
  36. package/dist/adapters/process/node-process-runner.d.ts +8 -0
  37. package/dist/adapters/process/node-process-runner.js +155 -0
  38. package/dist/adapters/prompt/inquirer-generation-prompt.d.ts +55 -0
  39. package/dist/adapters/prompt/inquirer-generation-prompt.js +213 -0
  40. package/dist/adapters/prompt/inquirer-init-prompt.d.ts +32 -0
  41. package/dist/adapters/prompt/inquirer-init-prompt.js +55 -0
  42. package/dist/adapters/prompt/inquirer-recorder-prompt.d.ts +47 -0
  43. package/dist/adapters/prompt/inquirer-recorder-prompt.js +157 -0
  44. package/dist/application/assertion/expectation-evaluator.d.ts +61 -0
  45. package/dist/application/assertion/expectation-evaluator.js +207 -0
  46. package/dist/application/collector/logcat-collector.d.ts +39 -0
  47. package/dist/application/collector/logcat-collector.js +107 -0
  48. package/dist/application/context/context-validator.d.ts +27 -0
  49. package/dist/application/context/context-validator.js +120 -0
  50. package/dist/application/doctor/doctor-service.d.ts +35 -0
  51. package/dist/application/doctor/doctor-service.js +178 -0
  52. package/dist/application/generation/focused-input.d.ts +2 -0
  53. package/dist/application/generation/focused-input.js +9 -0
  54. package/dist/application/generation/generation-confirmation-service.d.ts +76 -0
  55. package/dist/application/generation/generation-confirmation-service.js +383 -0
  56. package/dist/application/generation/generation-finalizer.d.ts +67 -0
  57. package/dist/application/generation/generation-finalizer.js +646 -0
  58. package/dist/application/generation/generation-publisher.d.ts +46 -0
  59. package/dist/application/generation/generation-publisher.js +178 -0
  60. package/dist/application/generation/generation-starter.d.ts +31 -0
  61. package/dist/application/generation/generation-starter.js +102 -0
  62. package/dist/application/generation/generation-step-executor.d.ts +52 -0
  63. package/dist/application/generation/generation-step-executor.js +668 -0
  64. package/dist/application/generation/proposed-step-validator.d.ts +11 -0
  65. package/dist/application/generation/proposed-step-validator.js +88 -0
  66. package/dist/application/generation/risk-evaluator.d.ts +9 -0
  67. package/dist/application/generation/risk-evaluator.js +14 -0
  68. package/dist/application/generation/runtime-observer.d.ts +39 -0
  69. package/dist/application/generation/runtime-observer.js +157 -0
  70. package/dist/application/init/init-service.d.ts +17 -0
  71. package/dist/application/init/init-service.js +44 -0
  72. package/dist/application/interaction/action-executor.d.ts +25 -0
  73. package/dist/application/interaction/action-executor.js +103 -0
  74. package/dist/application/interaction/fallback-resolver.d.ts +24 -0
  75. package/dist/application/interaction/fallback-resolver.js +48 -0
  76. package/dist/application/interaction/scroll-to-executor.d.ts +40 -0
  77. package/dist/application/interaction/scroll-to-executor.js +191 -0
  78. package/dist/application/locator/locator-resolver.d.ts +20 -0
  79. package/dist/application/locator/locator-resolver.js +89 -0
  80. package/dist/application/project/project-describer.d.ts +14 -0
  81. package/dist/application/project/project-describer.js +8 -0
  82. package/dist/application/recorder/locator-selector.d.ts +10 -0
  83. package/dist/application/recorder/locator-selector.js +80 -0
  84. package/dist/application/recorder/recorder-service.d.ts +41 -0
  85. package/dist/application/recorder/recorder-service.js +331 -0
  86. package/dist/application/report/report-writer.d.ts +10 -0
  87. package/dist/application/report/report-writer.js +39 -0
  88. package/dist/application/runtime/activity-waiter.d.ts +33 -0
  89. package/dist/application/runtime/activity-waiter.js +73 -0
  90. package/dist/application/runtime/launch-failure.d.ts +2 -0
  91. package/dist/application/runtime/launch-failure.js +20 -0
  92. package/dist/application/runtime/process-waiter.d.ts +27 -0
  93. package/dist/application/runtime/process-waiter.js +73 -0
  94. package/dist/application/runtime/step-runner.d.ts +50 -0
  95. package/dist/application/runtime/step-runner.js +429 -0
  96. package/dist/application/runtime/verify-runtime.d.ts +44 -0
  97. package/dist/application/runtime/verify-runtime.js +350 -0
  98. package/dist/application/wait/idle-waiter.d.ts +30 -0
  99. package/dist/application/wait/idle-waiter.js +90 -0
  100. package/dist/cli/commands/context.d.ts +3 -0
  101. package/dist/cli/commands/context.js +94 -0
  102. package/dist/cli/commands/doctor.d.ts +3 -0
  103. package/dist/cli/commands/doctor.js +51 -0
  104. package/dist/cli/commands/generation.d.ts +3 -0
  105. package/dist/cli/commands/generation.js +518 -0
  106. package/dist/cli/commands/init.d.ts +3 -0
  107. package/dist/cli/commands/init.js +104 -0
  108. package/dist/cli/commands/project.d.ts +3 -0
  109. package/dist/cli/commands/project.js +58 -0
  110. package/dist/cli/commands/record.d.ts +3 -0
  111. package/dist/cli/commands/record.js +88 -0
  112. package/dist/cli/commands/verify.d.ts +3 -0
  113. package/dist/cli/commands/verify.js +106 -0
  114. package/dist/cli/dependencies.d.ts +66 -0
  115. package/dist/cli/dependencies.js +204 -0
  116. package/dist/cli/main.d.ts +8 -0
  117. package/dist/cli/main.js +73 -0
  118. package/dist/cli/output.d.ts +16 -0
  119. package/dist/cli/output.js +22 -0
  120. package/dist/cli/program.d.ts +3 -0
  121. package/dist/cli/program.js +43 -0
  122. package/dist/domain/activity.d.ts +7 -0
  123. package/dist/domain/activity.js +65 -0
  124. package/dist/domain/app-process.d.ts +7 -0
  125. package/dist/domain/app-process.js +10 -0
  126. package/dist/domain/config.d.ts +15 -0
  127. package/dist/domain/config.js +17 -0
  128. package/dist/domain/failure.d.ts +4 -0
  129. package/dist/domain/failure.js +43 -0
  130. package/dist/domain/generation.d.ts +912 -0
  131. package/dist/domain/generation.js +342 -0
  132. package/dist/domain/init.d.ts +42 -0
  133. package/dist/domain/init.js +114 -0
  134. package/dist/domain/journey.d.ts +673 -0
  135. package/dist/domain/journey.js +109 -0
  136. package/dist/domain/layout.d.ts +35 -0
  137. package/dist/domain/layout.js +33 -0
  138. package/dist/domain/locator.d.ts +8 -0
  139. package/dist/domain/locator.js +5 -0
  140. package/dist/domain/project-context.d.ts +101 -0
  141. package/dist/domain/project-context.js +64 -0
  142. package/dist/domain/proposed-step.d.ts +341 -0
  143. package/dist/domain/proposed-step.js +87 -0
  144. package/dist/domain/report.d.ts +349 -0
  145. package/dist/domain/report.js +135 -0
  146. package/dist/domain/runtime-snapshot.d.ts +16 -0
  147. package/dist/domain/runtime-snapshot.js +46 -0
  148. package/dist/ports/adb.d.ts +42 -0
  149. package/dist/ports/adb.js +1 -0
  150. package/dist/ports/android-cli.d.ts +21 -0
  151. package/dist/ports/android-cli.js +1 -0
  152. package/dist/ports/artifact-store.d.ts +12 -0
  153. package/dist/ports/artifact-store.js +1 -0
  154. package/dist/ports/clock.d.ts +4 -0
  155. package/dist/ports/clock.js +1 -0
  156. package/dist/ports/generation-meta-writer.d.ts +10 -0
  157. package/dist/ports/generation-meta-writer.js +1 -0
  158. package/dist/ports/generation-prompt.d.ts +19 -0
  159. package/dist/ports/generation-prompt.js +6 -0
  160. package/dist/ports/generation-session-store.d.ts +32 -0
  161. package/dist/ports/generation-session-store.js +26 -0
  162. package/dist/ports/init-prompt.d.ts +8 -0
  163. package/dist/ports/init-prompt.js +6 -0
  164. package/dist/ports/journey-writer.d.ts +10 -0
  165. package/dist/ports/journey-writer.js +1 -0
  166. package/dist/ports/process-runner.d.ts +32 -0
  167. package/dist/ports/process-runner.js +1 -0
  168. package/dist/ports/project-bound-file.d.ts +8 -0
  169. package/dist/ports/project-bound-file.js +1 -0
  170. package/dist/ports/project-file-inspector.d.ts +33 -0
  171. package/dist/ports/project-file-inspector.js +1 -0
  172. package/dist/ports/recorder-prompt.d.ts +29 -0
  173. package/dist/ports/recorder-prompt.js +1 -0
  174. package/dist/ports/skill-installer.d.ts +8 -0
  175. package/dist/ports/skill-installer.js +1 -0
  176. package/package.json +64 -0
@@ -0,0 +1,191 @@
1
+ import { resolveLocator } from "../locator/locator-resolver.js";
2
+ function isCancelled(signal) {
3
+ return signal?.aborted === true;
4
+ }
5
+ function callbackFailure(error, swipesUsed) {
6
+ if (error !== null
7
+ && typeof error === "object"
8
+ && "code" in error
9
+ && typeof error.code === "string"
10
+ && [
11
+ "ACTIVITY_BEFORE_MISMATCH",
12
+ "ACTIVITY_AFTER_MISMATCH",
13
+ "ACTION_FAILED"
14
+ ].includes(error.code)) {
15
+ return {
16
+ status: "failed",
17
+ code: error.code,
18
+ message: error instanceof Error ? error.message : error.code,
19
+ swipesUsed
20
+ };
21
+ }
22
+ return undefined;
23
+ }
24
+ export class ScrollToExecutor {
25
+ options;
26
+ constructor(options) {
27
+ this.options = options;
28
+ }
29
+ async execute(step, signal, initialLayout) {
30
+ let swipesUsed = 0;
31
+ let layout = initialLayout;
32
+ for (;;) {
33
+ if (isCancelled(signal)) {
34
+ return { status: "cancelled", swipesUsed };
35
+ }
36
+ try {
37
+ layout ??= this.options.readLayout === undefined
38
+ ? await this.options.androidCli.layout({
39
+ deviceSerial: this.options.deviceSerial,
40
+ ...(signal === undefined ? {} : { signal }),
41
+ timeoutMs: this.options.idle.timeoutMs
42
+ })
43
+ : await this.options.readLayout();
44
+ }
45
+ catch (error) {
46
+ const failed = callbackFailure(error, swipesUsed);
47
+ if (failed !== undefined)
48
+ return failed;
49
+ throw error;
50
+ }
51
+ if (isCancelled(signal)) {
52
+ return { status: "cancelled", swipesUsed };
53
+ }
54
+ const target = resolveLocator(layout, step.locator, { requireEnabled: false });
55
+ if (target.status === "found") {
56
+ return { status: "found", swipesUsed };
57
+ }
58
+ if (target.code === "LOCATOR_AMBIGUOUS") {
59
+ return {
60
+ status: "failed",
61
+ code: "LOCATOR_AMBIGUOUS",
62
+ message: target.message,
63
+ swipesUsed
64
+ };
65
+ }
66
+ if (swipesUsed >= step.maxSwipes) {
67
+ return {
68
+ status: "failed",
69
+ code: "SCROLL_TARGET_NOT_FOUND",
70
+ message: `Target not visible after ${String(step.maxSwipes)} swipes`,
71
+ swipesUsed
72
+ };
73
+ }
74
+ let container = resolveLocator(layout, step.container, { requireEnabled: false });
75
+ if (container.status !== "found") {
76
+ return {
77
+ status: "failed",
78
+ code: container.code,
79
+ message: container.message,
80
+ swipesUsed
81
+ };
82
+ }
83
+ if (container.element.bounds === undefined) {
84
+ return {
85
+ status: "failed",
86
+ code: "ACTION_FAILED",
87
+ message: "scroll container has no bounds to swipe",
88
+ swipesUsed
89
+ };
90
+ }
91
+ if (this.options.beforeSwipe !== undefined) {
92
+ try {
93
+ layout = await this.options.beforeSwipe();
94
+ }
95
+ catch (error) {
96
+ const failed = callbackFailure(error, swipesUsed);
97
+ if (failed !== undefined)
98
+ return failed;
99
+ throw error;
100
+ }
101
+ if (isCancelled(signal)) {
102
+ return { status: "cancelled", swipesUsed };
103
+ }
104
+ const liveTarget = resolveLocator(layout, step.locator, { requireEnabled: false });
105
+ if (liveTarget.status === "found") {
106
+ return { status: "found", swipesUsed };
107
+ }
108
+ if (liveTarget.code === "LOCATOR_AMBIGUOUS") {
109
+ return {
110
+ status: "failed",
111
+ code: "LOCATOR_AMBIGUOUS",
112
+ message: liveTarget.message,
113
+ swipesUsed
114
+ };
115
+ }
116
+ container = resolveLocator(layout, step.container, { requireEnabled: false });
117
+ if (container.status !== "found") {
118
+ return {
119
+ status: "failed",
120
+ code: container.code,
121
+ message: container.message,
122
+ swipesUsed
123
+ };
124
+ }
125
+ if (container.element.bounds === undefined) {
126
+ return {
127
+ status: "failed",
128
+ code: "ACTION_FAILED",
129
+ message: "scroll container has no bounds to swipe",
130
+ swipesUsed
131
+ };
132
+ }
133
+ }
134
+ if (isCancelled(signal)) {
135
+ return { status: "cancelled", swipesUsed };
136
+ }
137
+ if (this.options.requireLiveContainerCapability === true
138
+ && (!container.element.enabled
139
+ || container.element.scrollable !== true)) {
140
+ return {
141
+ status: "failed",
142
+ code: "ACTION_FAILED",
143
+ message: "scroll container lost enabled scrollable bounds",
144
+ swipesUsed
145
+ };
146
+ }
147
+ try {
148
+ await this.options.beforeMutation?.();
149
+ }
150
+ catch (error) {
151
+ const failed = callbackFailure(error, swipesUsed);
152
+ if (failed !== undefined)
153
+ return failed;
154
+ throw error;
155
+ }
156
+ if (isCancelled(signal)) {
157
+ return { status: "cancelled", swipesUsed };
158
+ }
159
+ const swipe = await this.options.actionExecutor.swipeBounds(container.element.bounds, step.direction, step.distancePercent, step.durationMs, signal);
160
+ if (isCancelled(signal)) {
161
+ return { status: "cancelled", swipesUsed };
162
+ }
163
+ if (swipe.status === "failed") {
164
+ return {
165
+ status: "failed",
166
+ code: swipe.code,
167
+ message: swipe.message,
168
+ swipesUsed
169
+ };
170
+ }
171
+ const idle = await this.options.idleWaiter.waitUntilIdle(this.options.idle, signal);
172
+ if (isCancelled(signal)) {
173
+ return { status: "cancelled", swipesUsed };
174
+ }
175
+ if (idle.status === "cancelled") {
176
+ return { status: "cancelled", swipesUsed };
177
+ }
178
+ if (idle.status === "timeout") {
179
+ return {
180
+ status: "failed",
181
+ code: idle.code,
182
+ message: "Layout did not become stable before timeout",
183
+ swipesUsed,
184
+ idle: { polls: idle.polls, lastDiff: idle.lastDiff }
185
+ };
186
+ }
187
+ swipesUsed += 1;
188
+ layout = undefined;
189
+ }
190
+ }
191
+ }
@@ -0,0 +1,20 @@
1
+ import type { FailureCode } from "../../domain/failure.js";
2
+ import { type LocatorField } from "../../domain/locator.js";
3
+ import type { LayoutElement, Locator } from "../../domain/layout.js";
4
+ import type { Point } from "../../ports/android-cli.js";
5
+ export interface LocatedTarget {
6
+ status: "found";
7
+ element: LayoutElement;
8
+ point: Point;
9
+ matchedBy: LocatorField;
10
+ }
11
+ export interface LocatorFailure {
12
+ status: "failed";
13
+ code: Extract<FailureCode, "LOCATOR_NOT_FOUND" | "LOCATOR_AMBIGUOUS" | "ACTION_FAILED">;
14
+ message: string;
15
+ }
16
+ export type LocatorResolution = LocatedTarget | LocatorFailure;
17
+ export interface LocatorResolutionOptions {
18
+ requireEnabled?: boolean | undefined;
19
+ }
20
+ export declare function resolveLocator(roots: readonly LayoutElement[], locator: Locator, options?: LocatorResolutionOptions): LocatorResolution;
@@ -0,0 +1,89 @@
1
+ import { LOCATOR_FIELDS } from "../../domain/locator.js";
2
+ function flatten(elements) {
3
+ return elements.flatMap((element) => [
4
+ element,
5
+ ...flatten(element.children)
6
+ ]);
7
+ }
8
+ function center(element) {
9
+ if (element.center !== undefined) {
10
+ return element.center;
11
+ }
12
+ const bounds = element.bounds;
13
+ if (bounds === undefined) {
14
+ throw new Error(`Layout element ${element.id} has no center or bounds`);
15
+ }
16
+ return {
17
+ x: Math.round((bounds.left + bounds.right) / 2),
18
+ y: Math.round((bounds.top + bounds.bottom) / 2)
19
+ };
20
+ }
21
+ export function resolveLocator(roots, locator, options = {}) {
22
+ const elements = flatten(roots);
23
+ let candidates;
24
+ let matchedBy;
25
+ for (const field of LOCATOR_FIELDS) {
26
+ const value = locator[field];
27
+ if (value === undefined) {
28
+ continue;
29
+ }
30
+ if (candidates === undefined) {
31
+ const matches = elements.filter((element) => element[field] === value);
32
+ if (matches.length === 0) {
33
+ continue;
34
+ }
35
+ candidates = matches;
36
+ matchedBy = field;
37
+ }
38
+ else if (candidates.length > 1) {
39
+ const narrowed = candidates.filter((element) => element[field] === value);
40
+ if (narrowed.length === 0) {
41
+ return {
42
+ status: "failed",
43
+ code: "LOCATOR_NOT_FOUND",
44
+ message: `Locator fields conflict at ${field}`
45
+ };
46
+ }
47
+ candidates = narrowed;
48
+ matchedBy = field;
49
+ }
50
+ if (candidates.length === 1) {
51
+ break;
52
+ }
53
+ }
54
+ if (candidates === undefined || candidates.length === 0) {
55
+ return {
56
+ status: "failed",
57
+ code: "LOCATOR_NOT_FOUND",
58
+ message: "No Layout element matches the Locator"
59
+ };
60
+ }
61
+ if (candidates.length > 1) {
62
+ return {
63
+ status: "failed",
64
+ code: "LOCATOR_AMBIGUOUS",
65
+ message: `Locator matches ${String(candidates.length)} Layout elements`
66
+ };
67
+ }
68
+ const element = candidates[0];
69
+ if (element === undefined || matchedBy === undefined) {
70
+ return {
71
+ status: "failed",
72
+ code: "LOCATOR_NOT_FOUND",
73
+ message: "No Layout element matches the Locator"
74
+ };
75
+ }
76
+ if (options.requireEnabled !== false && !element.enabled) {
77
+ return {
78
+ status: "failed",
79
+ code: "ACTION_FAILED",
80
+ message: `Layout element ${element.id} is disabled`
81
+ };
82
+ }
83
+ return {
84
+ status: "found",
85
+ element,
86
+ point: center(element),
87
+ matchedBy
88
+ };
89
+ }
@@ -0,0 +1,14 @@
1
+ import type { TapHoundConfig } from "../../domain/config.js";
2
+ export interface ProjectDescribeInput {
3
+ projectRoot: string;
4
+ config: TapHoundConfig;
5
+ signal?: AbortSignal | undefined;
6
+ }
7
+ export interface ProjectDescription {
8
+ projectRoot: string;
9
+ packageName: string;
10
+ launchActivity: string;
11
+ }
12
+ export declare class ProjectDescriber {
13
+ readonly describe: (input: ProjectDescribeInput) => Promise<ProjectDescription>;
14
+ }
@@ -0,0 +1,8 @@
1
+ import { normalizeActivity } from "../../domain/activity.js";
2
+ export class ProjectDescriber {
3
+ describe = async (input) => Promise.resolve({
4
+ projectRoot: input.projectRoot,
5
+ packageName: input.config.run.packageName,
6
+ launchActivity: normalizeActivity(input.config.run.packageName, input.config.run.activity)
7
+ });
8
+ }
@@ -0,0 +1,10 @@
1
+ import type { LayoutElement, Locator } from "../../domain/layout.js";
2
+ export interface RecorderTarget {
3
+ element: LayoutElement;
4
+ locator: Locator;
5
+ label: string;
6
+ }
7
+ export type RecorderTargetAction = "click" | "longClick" | "swipe";
8
+ export declare function selectUniqueLocator(target: LayoutElement, roots: readonly LayoutElement[]): Locator | undefined;
9
+ export declare function listRecorderTargets(roots: readonly LayoutElement[], action: RecorderTargetAction): RecorderTarget[];
10
+ export declare function listLocatableTargets(roots: readonly LayoutElement[]): RecorderTarget[];
@@ -0,0 +1,80 @@
1
+ import { LOCATOR_FIELDS } from "../../domain/locator.js";
2
+ function flatten(elements) {
3
+ return elements.flatMap((element) => [
4
+ element,
5
+ ...flatten(element.children)
6
+ ]);
7
+ }
8
+ export function selectUniqueLocator(target, roots) {
9
+ const elements = flatten(roots);
10
+ for (const field of LOCATOR_FIELDS) {
11
+ const value = target[field];
12
+ if (value !== undefined
13
+ && value.length > 0
14
+ && elements.filter((element) => element[field] === value).length === 1) {
15
+ return { [field]: value };
16
+ }
17
+ }
18
+ return undefined;
19
+ }
20
+ function targetLabel(element, locator) {
21
+ const identity = LOCATOR_FIELDS.find((field) => locator[field] !== undefined);
22
+ if (identity === undefined) {
23
+ return element.id;
24
+ }
25
+ const value = locator[identity] ?? element.id;
26
+ return `${element.id} — ${identity}: ${value}`;
27
+ }
28
+ function supportsAction(element, action) {
29
+ return action === "click"
30
+ ? element.clickable === true
31
+ : action === "longClick"
32
+ ? element.longClickable === true
33
+ : element.scrollable === true && element.bounds !== undefined;
34
+ }
35
+ export function listRecorderTargets(roots, action) {
36
+ const primary = flatten(roots).flatMap((element) => {
37
+ if (!element.enabled || !supportsAction(element, action)) {
38
+ return [];
39
+ }
40
+ const locator = selectUniqueLocator(element, roots);
41
+ if (locator === undefined) {
42
+ return [];
43
+ }
44
+ return [{
45
+ element,
46
+ locator,
47
+ label: targetLabel(element, locator)
48
+ }];
49
+ });
50
+ // Relaxed content targets are click-only: for longClick the interactive
51
+ // target is often a small bounded view (e.g. a chat bubble), so a sibling
52
+ // label's center coordinate hit-tests to a neighbouring element instead.
53
+ if (action !== "click") {
54
+ return primary;
55
+ }
56
+ const hasOrphan = flatten(roots).some((element) => element.enabled
57
+ && supportsAction(element, action)
58
+ && selectUniqueLocator(element, roots) === undefined);
59
+ if (!hasOrphan) {
60
+ return primary;
61
+ }
62
+ const primaryIds = new Set(primary.map((target) => target.element.id));
63
+ const relaxed = listLocatableTargets(roots).filter((target) => !supportsAction(target.element, action)
64
+ && !primaryIds.has(target.element.id)
65
+ && (target.locator.text !== undefined
66
+ || target.locator.contentDescription !== undefined));
67
+ return [...primary, ...relaxed];
68
+ }
69
+ export function listLocatableTargets(roots) {
70
+ return flatten(roots).flatMap((element) => {
71
+ if (!element.enabled) {
72
+ return [];
73
+ }
74
+ const locator = selectUniqueLocator(element, roots);
75
+ if (locator === undefined) {
76
+ return [];
77
+ }
78
+ return [{ element, locator, label: targetLabel(element, locator) }];
79
+ });
80
+ }
@@ -0,0 +1,41 @@
1
+ import type { TapHoundConfig } from "../../domain/config.js";
2
+ import { type Journey } from "../../domain/journey.js";
3
+ import type { AdbPort } from "../../ports/adb.js";
4
+ import type { AndroidCliPort } from "../../ports/android-cli.js";
5
+ import type { Clock } from "../../ports/clock.js";
6
+ import type { JourneyWriterPort } from "../../ports/journey-writer.js";
7
+ import type { RecorderPromptPort } from "../../ports/recorder-prompt.js";
8
+ export interface RecorderDependencies {
9
+ androidCli: AndroidCliPort;
10
+ adb: AdbPort;
11
+ clock: Clock;
12
+ prompt: RecorderPromptPort;
13
+ journeyWriter: JourneyWriterPort;
14
+ }
15
+ export interface RecordInput {
16
+ config: TapHoundConfig;
17
+ projectRoot: string;
18
+ deviceSerial: string;
19
+ journeyName: string;
20
+ outputPath: string;
21
+ signal?: AbortSignal | undefined;
22
+ }
23
+ export type RecordResult = {
24
+ status: "completed";
25
+ stepsRecorded: number;
26
+ journey: Journey;
27
+ } | {
28
+ status: "cancelled";
29
+ stepsRecorded: number;
30
+ } | {
31
+ status: "failed";
32
+ stepsRecorded: number;
33
+ message: string;
34
+ };
35
+ export declare class RecorderService {
36
+ private readonly dependencies;
37
+ constructor(dependencies: RecorderDependencies);
38
+ record(input: RecordInput): Promise<RecordResult>;
39
+ private prepareAction;
40
+ private prepareScrollTo;
41
+ }