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,646 @@
1
+ import { createHash } from "node:crypto";
2
+ import { realpath } from "node:fs/promises";
3
+ import { basename, extname } from "node:path";
4
+ import { z } from "zod";
5
+ import { normalizeActivity } from "../../domain/activity.js";
6
+ import { GenerationMetaSchema, GenerationReportSchema, GenerationSessionSchema } from "../../domain/generation.js";
7
+ import { JourneySchema } from "../../domain/journey.js";
8
+ import { ProjectContextSchema, ProjectRelativePathSchema } from "../../domain/project-context.js";
9
+ import { hashJourney, TapHoundReportSchema } from "../../domain/report.js";
10
+ import { TapHoundConfigSchema } from "../../domain/config.js";
11
+ import { GenerationSessionStoreError } from "../../ports/generation-session-store.js";
12
+ import { GENERATION_BUNDLE_PATHS } from "./generation-publisher.js";
13
+ import { hashGenerationBinding } from "./generation-starter.js";
14
+ const Sha256Schema = z.string().regex(/^[a-f\d]{64}$/);
15
+ const ProjectDescriptionSchema = z.strictObject({
16
+ projectRoot: z.string().min(1),
17
+ packageName: z.string().min(1),
18
+ launchActivity: z.string().min(1)
19
+ });
20
+ const VerificationReceiptSchema = z.strictObject({
21
+ version: z.literal(1),
22
+ generationId: z.string().min(1),
23
+ attemptId: z.string().min(1),
24
+ journey: z.strictObject({
25
+ name: z.string().min(1),
26
+ sha256: Sha256Schema
27
+ }),
28
+ project: z.strictObject({
29
+ root: z.string().min(1),
30
+ packageName: z.string().min(1),
31
+ launchActivity: z.string().min(1)
32
+ }),
33
+ deviceSerial: z.string().min(1),
34
+ bindings: z.strictObject({
35
+ projectHash: Sha256Schema,
36
+ configHash: Sha256Schema,
37
+ contextHash: Sha256Schema,
38
+ snapshotHash: Sha256Schema.nullable()
39
+ }),
40
+ tools: z.record(z.string(), z.string()),
41
+ report: z.strictObject({
42
+ path: z.literal(GENERATION_BUNDLE_PATHS.verificationReport),
43
+ sha256: Sha256Schema,
44
+ runId: z.string().min(1)
45
+ })
46
+ });
47
+ export const GenerationOutputPathSchema = ProjectRelativePathSchema.refine((path) => path.split("/").every((segment) => segment.length > 0 && segment !== "."), "Generation output path must be normalized").refine((path) => (path !== ".taphound/generations"
48
+ && !path.startsWith(".taphound/generations/")), "Generation output cannot overlap the authoritative bundle");
49
+ export class GenerationFinalizationError extends Error {
50
+ code;
51
+ stage;
52
+ recoverable;
53
+ name = "GenerationFinalizationError";
54
+ constructor(code, stage, recoverable, message, options) {
55
+ super(message, options);
56
+ this.code = code;
57
+ this.stage = stage;
58
+ this.recoverable = recoverable;
59
+ }
60
+ }
61
+ function serializeJson(value) {
62
+ return `${JSON.stringify(value, null, 2)}\n`;
63
+ }
64
+ function sha256(value) {
65
+ return createHash("sha256").update(value).digest("hex");
66
+ }
67
+ function commandFailed(result) {
68
+ return result.exitCode !== 0
69
+ || result.timedOut
70
+ || result.cancelled
71
+ || result.spawnError !== undefined;
72
+ }
73
+ function derivedJourneyName(outputPath) {
74
+ const file = basename(outputPath);
75
+ const extension = extname(file);
76
+ const stem = extension.length === 0 ? file : file.slice(0, -extension.length);
77
+ return stem.trim() || "Generated Journey";
78
+ }
79
+ function sameSession(left, right) {
80
+ return JSON.stringify(left) === JSON.stringify(right);
81
+ }
82
+ function canonicalTools(input) {
83
+ return Object.fromEntries(Object.entries(z.record(z.string(), z.string()).parse(input))
84
+ .sort(([left], [right]) => left.localeCompare(right)));
85
+ }
86
+ function sameJson(left, right) {
87
+ return JSON.stringify(left) === JSON.stringify(right);
88
+ }
89
+ function locatorMatchIsEligible(matchedBy, step) {
90
+ if (step.action !== "click"
91
+ && step.action !== "longClick"
92
+ && step.action !== "swipe") {
93
+ return matchedBy === undefined;
94
+ }
95
+ return matchedBy !== undefined && step.locator[matchedBy] !== undefined;
96
+ }
97
+ function failure(code, stage, message, recoverable = false, cause) {
98
+ return new GenerationFinalizationError(code, stage, recoverable, message, cause === undefined ? undefined : { cause });
99
+ }
100
+ export class GenerationFinalizer {
101
+ dependencies;
102
+ constructor(dependencies) {
103
+ this.dependencies = dependencies;
104
+ }
105
+ finalize = async (input) => {
106
+ const config = TapHoundConfigSchema.parse(input.config);
107
+ const context = ProjectContextSchema.parse(input.context);
108
+ const project = ProjectDescriptionSchema.parse(input.project);
109
+ const canonicalProjectRoot = await realpath(input.projectRoot);
110
+ const outputPath = GenerationOutputPathSchema.parse(input.outputPath);
111
+ const name = input.name === undefined
112
+ ? derivedJourneyName(outputPath)
113
+ : z.string().trim().min(1).parse(input.name);
114
+ let session = GenerationSessionSchema.parse(await this.dependencies.store.read(input.generationId));
115
+ this.assertBindings(session, input, config, context, project);
116
+ const journey = JourneySchema.parse({
117
+ version: 1,
118
+ name,
119
+ steps: session.candidateSteps
120
+ });
121
+ let replayed = false;
122
+ let verificationReport;
123
+ if (session.verification.status === "notRun") {
124
+ const begun = await this.beginVerification(session);
125
+ session = begun.session;
126
+ if (!begun.owned
127
+ && session.verification.status === "failed") {
128
+ throw failure("VERIFICATION_FAILED", "verification", session.verification.failure.message);
129
+ }
130
+ const expected = this.expectedVerification(session, journey, config, canonicalProjectRoot, input.toolVersions);
131
+ if (!begun.owned) {
132
+ if (session.verification.status === "passed") {
133
+ verificationReport = await this.readVerifiedReport(session, expected);
134
+ }
135
+ else {
136
+ let reconciled;
137
+ try {
138
+ reconciled = session.verification.status === "running"
139
+ ? await this.reconcilePassedVerification(session, expected, () => this.revalidate(input, config, context, project, session))
140
+ : undefined;
141
+ }
142
+ catch (error) {
143
+ await this.persistFailedVerification(session, error);
144
+ throw failure("VERIFICATION_FAILED", "verification", error instanceof Error
145
+ ? error.message
146
+ : "Immutable verification receipt is invalid", false, error);
147
+ }
148
+ if (reconciled === undefined) {
149
+ throw failure("FINALIZATION_IN_PROGRESS", "verification", "Another finalizer owns verification", true);
150
+ }
151
+ session = reconciled.session;
152
+ verificationReport = reconciled.report;
153
+ }
154
+ }
155
+ if (begun.owned) {
156
+ try {
157
+ await this.revalidate(input, config, context, project, session);
158
+ if (input.signal?.aborted === true) {
159
+ throw failure("VERIFICATION_FAILED", "verification", "Generation finalization was cancelled");
160
+ }
161
+ const stopped = await this.dependencies.adb.forceStop({
162
+ packageName: session.target.packageName,
163
+ deviceSerial: session.target.deviceSerial,
164
+ ...(input.signal === undefined ? {} : { signal: input.signal }),
165
+ timeoutMs: config.idle.timeoutMs
166
+ });
167
+ if (commandFailed(stopped)) {
168
+ throw failure("VERIFICATION_FAILED", "verification", stopped.stderr.trim()
169
+ || stopped.spawnError
170
+ || "Unable to reset the generated Journey process");
171
+ }
172
+ const result = await this.dependencies.verifyRuntime.verify({
173
+ config,
174
+ journey,
175
+ projectRoot: canonicalProjectRoot,
176
+ deviceSerial: input.deviceSerial,
177
+ toolVersions: input.toolVersions,
178
+ requireFocusedInput: true,
179
+ generatedReplayPolicy: true,
180
+ ...(input.signal === undefined ? {} : { signal: input.signal })
181
+ });
182
+ replayed = true;
183
+ await this.revalidate(input, config, context, project, session);
184
+ this.assertEligible(result, journey, expected);
185
+ verificationReport = TapHoundReportSchema.parse(result.report);
186
+ session = await this.persistPassedVerification(session, verificationReport, expected);
187
+ }
188
+ catch (error) {
189
+ if (session.verification.status === "running"
190
+ && !(error instanceof GenerationFinalizationError
191
+ && error.code === "FINALIZATION_IN_PROGRESS")) {
192
+ await this.persistFailedVerification(session, error);
193
+ }
194
+ throw error instanceof GenerationFinalizationError
195
+ ? error
196
+ : failure("VERIFICATION_FAILED", "verification", error instanceof Error
197
+ ? error.message
198
+ : "Generation verification failed", false, error);
199
+ }
200
+ }
201
+ }
202
+ else if (session.verification.status === "running") {
203
+ const expected = this.expectedVerification(session, journey, config, canonicalProjectRoot, input.toolVersions);
204
+ let reconciled;
205
+ try {
206
+ reconciled = await this.reconcilePassedVerification(session, expected, () => this.revalidate(input, config, context, project, session));
207
+ }
208
+ catch (error) {
209
+ await this.persistFailedVerification(session, error);
210
+ throw failure("VERIFICATION_FAILED", "verification", error instanceof Error
211
+ ? error.message
212
+ : "Immutable verification receipt is invalid", false, error);
213
+ }
214
+ if (reconciled === undefined) {
215
+ throw failure("FINALIZATION_IN_PROGRESS", "verification", "Generation verification is already running or cannot be safely reconciled", true);
216
+ }
217
+ session = reconciled.session;
218
+ verificationReport = reconciled.report;
219
+ }
220
+ else if (session.verification.status === "failed") {
221
+ throw failure("VERIFICATION_FAILED", "verification", session.verification.failure.message);
222
+ }
223
+ else {
224
+ verificationReport = await this.readVerifiedReport(session, this.expectedVerification(session, journey, config, canonicalProjectRoot, input.toolVersions));
225
+ }
226
+ if (verificationReport === undefined) {
227
+ throw failure("RECOVERY_REQUIRED", "verification", "Verification completed without durable report evidence");
228
+ }
229
+ this.assertReport(verificationReport, journey, this.expectedVerification(session, journey, config, canonicalProjectRoot, input.toolVersions), "RECOVERY_REQUIRED");
230
+ this.assertBindings(session, input, config, context, project);
231
+ const meta = this.buildMeta(session, outputPath, verificationReport);
232
+ const generationReport = GenerationReportSchema.parse({
233
+ version: 1,
234
+ generationId: session.id,
235
+ status: "verified",
236
+ steps: session.candidateSources.map((source, index) => ({
237
+ index,
238
+ source
239
+ }))
240
+ });
241
+ if (session.publication.status === "notRun") {
242
+ try {
243
+ await this.dependencies.publisher.stage({
244
+ generationId: session.id,
245
+ journey,
246
+ report: generationReport,
247
+ verificationReport,
248
+ meta
249
+ });
250
+ const publishable = GenerationSessionSchema.parse({
251
+ ...session,
252
+ revision: session.revision + 1,
253
+ publication: {
254
+ status: "published",
255
+ journeyPath: outputPath
256
+ }
257
+ });
258
+ try {
259
+ await this.dependencies.store.markBundlePublishable(session.id, session.revision, publishable);
260
+ }
261
+ catch (error) {
262
+ const latest = GenerationSessionSchema.parse(await this.dependencies.store.read(session.id));
263
+ if (!sameSession(latest, publishable)) {
264
+ throw error;
265
+ }
266
+ }
267
+ session = publishable;
268
+ }
269
+ catch (error) {
270
+ throw failure("PUBLICATION_FAILED", "publication", error instanceof Error ? error.message : "Bundle staging failed", true, error);
271
+ }
272
+ }
273
+ else if (session.publication.status !== "published"
274
+ || session.publication.journeyPath !== outputPath) {
275
+ throw failure("PUBLICATION_FAILED", "publication", "Published generation output path does not match the requested path");
276
+ }
277
+ let bundlePath;
278
+ try {
279
+ bundlePath = await this.dependencies.publisher.publish(session.id);
280
+ }
281
+ catch (error) {
282
+ try {
283
+ bundlePath = await this.dependencies.publisher.publish(session.id);
284
+ }
285
+ catch {
286
+ throw failure("PUBLICATION_FAILED", "publication", "Authoritative generation bundle publication is ambiguous", true, error);
287
+ }
288
+ }
289
+ try {
290
+ const exported = await this.dependencies.publisher.export({
291
+ generationId: session.id,
292
+ projectRoot: input.projectRoot,
293
+ journeyPath: outputPath,
294
+ journey,
295
+ meta
296
+ });
297
+ return {
298
+ status: "verified",
299
+ journey,
300
+ meta,
301
+ bundlePath,
302
+ journeyPath: exported.journeyPath,
303
+ metaPath: exported.metaPath,
304
+ replayed
305
+ };
306
+ }
307
+ catch (error) {
308
+ throw failure("EXPORT_FAILED", "export", "Authoritative bundle was published but Journey export failed", true, error);
309
+ }
310
+ };
311
+ assertBindings(session, input, config, context, project) {
312
+ if (input.projectRoot !== project.projectRoot
313
+ || input.deviceSerial !== session.target.deviceSerial
314
+ || project.packageName !== session.target.packageName
315
+ || session.target.packageName !== config.run.packageName
316
+ || normalizeActivity(project.packageName, project.launchActivity) !== normalizeActivity(config.run.packageName, config.run.activity)
317
+ || session.inFlight !== null
318
+ || session.pendingConfirmation !== null
319
+ || session.state !== "active"
320
+ || session.bindings.projectHash !== hashGenerationBinding(project)
321
+ || session.bindings.configHash !== hashGenerationBinding(config)
322
+ || session.bindings.contextHash !== hashGenerationBinding(context)
323
+ || JSON.stringify(session.target.interactionPolicy)
324
+ !== JSON.stringify(context.interactionPolicy)) {
325
+ throw failure("CONTEXT_STALE", "precondition", "Generation identity, context, configuration, or process-only binding changed");
326
+ }
327
+ }
328
+ async revalidate(input, config, context, project, session) {
329
+ this.assertBindings(session, input, config, context, project);
330
+ const result = await this.dependencies.contextValidator.validate({
331
+ context,
332
+ projectRoot: input.projectRoot,
333
+ config
334
+ });
335
+ if (result.status !== "valid") {
336
+ throw failure(result.status === "stale" ? "CONTEXT_STALE" : "CONTEXT_INVALID", "precondition", result.reason.message);
337
+ }
338
+ }
339
+ async beginVerification(session) {
340
+ const attemptId = this.dependencies.generateAttemptId();
341
+ const expected = GenerationSessionSchema.parse({
342
+ ...session,
343
+ revision: session.revision + 1,
344
+ verification: { status: "running", attemptId }
345
+ });
346
+ try {
347
+ return {
348
+ session: await this.dependencies.store.beginVerification(session.id, session.revision, attemptId),
349
+ owned: true
350
+ };
351
+ }
352
+ catch (error) {
353
+ const latest = GenerationSessionSchema.parse(await this.dependencies.store.read(session.id));
354
+ if (sameSession(latest, expected)) {
355
+ return { session: latest, owned: true };
356
+ }
357
+ if (latest.verification.status !== "notRun") {
358
+ return { session: latest, owned: false };
359
+ }
360
+ throw error;
361
+ }
362
+ }
363
+ expectedVerification(session, journey, config, canonicalProjectRoot, tools) {
364
+ if (session.verification.status !== "running"
365
+ && session.verification.status !== "passed") {
366
+ throw failure("RECOVERY_REQUIRED", "verification", "Generation verification attempt identity is unavailable");
367
+ }
368
+ return {
369
+ generationId: session.id,
370
+ attemptId: session.verification.attemptId,
371
+ journey: {
372
+ name: journey.name,
373
+ sha256: hashJourney(journey)
374
+ },
375
+ project: {
376
+ root: canonicalProjectRoot,
377
+ packageName: session.target.packageName,
378
+ launchActivity: normalizeActivity(session.target.packageName, config.run.activity)
379
+ },
380
+ deviceSerial: session.target.deviceSerial,
381
+ bindings: { ...session.bindings },
382
+ tools: canonicalTools(tools)
383
+ };
384
+ }
385
+ assertEligible(result, journey, expected) {
386
+ if (result.status !== "passed"
387
+ || result.exitCode !== 0) {
388
+ throw failure("VERIFICATION_FAILED", "verification", "Generated replay did not pass cleanly");
389
+ }
390
+ this.assertReport(result.report, journey, expected, "VERIFICATION_FAILED");
391
+ }
392
+ assertReport(report, journey, expected, code) {
393
+ if (report.status !== "passed"
394
+ || report.fallbackUsed
395
+ || report.primaryFailure !== undefined
396
+ || report.secondaryErrors.length !== 0
397
+ || Object.values(report.layers).some((status) => status !== "passed")
398
+ || report.journey.name !== journey.name
399
+ || report.journey.sha256 !== hashJourney(journey)
400
+ || report.project.root !== expected.project.root
401
+ || report.project.packageName !== expected.project.packageName
402
+ || report.project.launchActivity !== expected.project.launchActivity
403
+ || report.environment.deviceSerial !== expected.deviceSerial
404
+ || !sameJson(canonicalTools(report.environment.tools), expected.tools)
405
+ || report.steps.length !== journey.steps.length) {
406
+ throw failure(code, "verification", "Verification report does not match the exact generated replay");
407
+ }
408
+ for (const [index, candidate] of journey.steps.entries()) {
409
+ const step = report.steps[index];
410
+ const expectationType = candidate.expect?.type;
411
+ const hasLocator = candidate.action === "click"
412
+ || candidate.action === "longClick"
413
+ || candidate.action === "swipe";
414
+ const isScrollTo = candidate.action === "scrollTo";
415
+ if (step === undefined
416
+ || step.index !== index
417
+ || step.action !== candidate.action
418
+ || step.status !== "passed"
419
+ || step.activity === undefined
420
+ || step.activity.before.status !== "passed"
421
+ || step.activity.before.expected !== candidate.activity.before
422
+ || step.activity.before.actual !== candidate.activity.before
423
+ || step.activity.after.status !== "passed"
424
+ || step.activity.after.expected !== candidate.activity.after
425
+ || step.activity.after.actual !== candidate.activity.after
426
+ || (expectationType === undefined
427
+ ? step.expectation !== undefined
428
+ : (step.expectation === undefined
429
+ || step.expectation.type !== expectationType
430
+ || step.expectation.status !== "passed"))
431
+ || (!hasLocator
432
+ ? step.locator !== undefined
433
+ : (step.locator === undefined
434
+ || step.locator.status !== "found"
435
+ || step.locator.fallbackUsed
436
+ || !locatorMatchIsEligible(step.locator.matchedBy, candidate)))
437
+ || step.locator?.fallbackUsed === true
438
+ || (isScrollTo
439
+ ? (step.scroll === undefined
440
+ || step.scroll.maxSwipes !== candidate.maxSwipes
441
+ || step.scroll.swipesUsed > candidate.maxSwipes
442
+ || step.idle !== undefined)
443
+ : (step.scroll !== undefined
444
+ || step.idle === undefined
445
+ || step.idle.status !== "stable"))) {
446
+ throw failure(code, "verification", `Verification report step ${String(index)} does not match the candidate`);
447
+ }
448
+ }
449
+ }
450
+ receiptMatches(receipt, expected) {
451
+ return receipt.generationId === expected.generationId
452
+ && receipt.attemptId === expected.attemptId
453
+ && sameJson(receipt.journey, expected.journey)
454
+ && sameJson(receipt.project, expected.project)
455
+ && receipt.deviceSerial === expected.deviceSerial
456
+ && sameJson(receipt.bindings, expected.bindings)
457
+ && sameJson(canonicalTools(receipt.tools), expected.tools);
458
+ }
459
+ async persistPassedVerification(running, report, expected) {
460
+ if (running.verification.status !== "running") {
461
+ throw failure("RECOVERY_REQUIRED", "verification", "Verification cannot pass without exact running-attempt ownership");
462
+ }
463
+ const reportBytes = serializeJson(report);
464
+ await this.ensureVerificationEvidence(running.id, GENERATION_BUNDLE_PATHS.verificationReport, reportBytes);
465
+ const receipt = VerificationReceiptSchema.parse({
466
+ version: 1,
467
+ ...expected,
468
+ report: {
469
+ path: GENERATION_BUNDLE_PATHS.verificationReport,
470
+ sha256: sha256(reportBytes),
471
+ runId: report.runId
472
+ }
473
+ });
474
+ await this.ensureVerificationEvidence(running.id, GENERATION_BUNDLE_PATHS.verificationReceipt, serializeJson(receipt));
475
+ const passed = GenerationSessionSchema.parse({
476
+ ...running,
477
+ revision: running.revision + 1,
478
+ verification: {
479
+ status: "passed",
480
+ attemptId: running.verification.attemptId,
481
+ reportPath: receipt.report.path,
482
+ reportSha256: receipt.report.sha256,
483
+ runId: receipt.report.runId
484
+ }
485
+ });
486
+ try {
487
+ await this.dependencies.store.completeVerification(running.id, running.revision, passed);
488
+ }
489
+ catch (error) {
490
+ const latest = GenerationSessionSchema.parse(await this.dependencies.store.read(running.id));
491
+ if (!sameSession(latest, passed)) {
492
+ throw error;
493
+ }
494
+ }
495
+ return passed;
496
+ }
497
+ async reconcilePassedVerification(running, expected, revalidateCurrent) {
498
+ if (running.verification.status !== "running") {
499
+ return undefined;
500
+ }
501
+ let receiptBytes;
502
+ try {
503
+ receiptBytes = await this.dependencies.store.readEvidence(running.id, GENERATION_BUNDLE_PATHS.verificationReceipt);
504
+ }
505
+ catch (error) {
506
+ if (error instanceof GenerationSessionStoreError
507
+ && error.code === "EVIDENCE_NOT_FOUND") {
508
+ return undefined;
509
+ }
510
+ throw failure("VERIFICATION_FAILED", "verification", "Immutable verification receipt cannot be safely read", false, error);
511
+ }
512
+ let receipt;
513
+ let reportBytes;
514
+ try {
515
+ receipt = VerificationReceiptSchema.parse(JSON.parse(receiptBytes.toString("utf8")));
516
+ reportBytes = await this.dependencies.store.readEvidence(running.id, receipt.report.path);
517
+ }
518
+ catch (error) {
519
+ throw failure("VERIFICATION_FAILED", "verification", "Immutable verification receipt or report is invalid", false, error);
520
+ }
521
+ if (!this.receiptMatches(receipt, expected)
522
+ || sha256(reportBytes) !== receipt.report.sha256) {
523
+ throw failure("VERIFICATION_FAILED", "verification", "Immutable verification receipt does not match this generation");
524
+ }
525
+ let report;
526
+ try {
527
+ report = TapHoundReportSchema.parse(JSON.parse(reportBytes.toString("utf8")));
528
+ if (report.runId !== receipt.report.runId) {
529
+ throw new Error("Verification Report run ID does not match receipt");
530
+ }
531
+ this.assertReport(report, {
532
+ version: 1,
533
+ name: expected.journey.name,
534
+ steps: running.candidateSteps
535
+ }, expected, "VERIFICATION_FAILED");
536
+ }
537
+ catch (error) {
538
+ throw error instanceof GenerationFinalizationError
539
+ ? error
540
+ : failure("VERIFICATION_FAILED", "verification", "Immutable verification Report does not match the receipt", false, error);
541
+ }
542
+ await revalidateCurrent();
543
+ return {
544
+ session: await this.persistPassedVerification(running, report, expected),
545
+ report
546
+ };
547
+ }
548
+ async persistFailedVerification(running, error) {
549
+ const latest = GenerationSessionSchema.parse(await this.dependencies.store.read(running.id));
550
+ if (running.verification.status !== "running"
551
+ || latest.verification.status !== "running"
552
+ || latest.revision !== running.revision
553
+ || latest.verification.attemptId !== running.verification.attemptId) {
554
+ return;
555
+ }
556
+ const failed = GenerationSessionSchema.parse({
557
+ ...latest,
558
+ revision: latest.revision + 1,
559
+ verification: {
560
+ status: "failed",
561
+ failure: {
562
+ code: error instanceof GenerationFinalizationError
563
+ ? error.code
564
+ : "VERIFICATION_FAILED",
565
+ message: error instanceof Error
566
+ ? error.message
567
+ : "Generation verification failed"
568
+ }
569
+ }
570
+ });
571
+ try {
572
+ await this.dependencies.store.failVerification(latest.id, latest.revision, failed);
573
+ }
574
+ catch {
575
+ const reconciled = GenerationSessionSchema.parse(await this.dependencies.store.read(latest.id));
576
+ if (!sameSession(reconciled, failed)) {
577
+ throw failure("RECOVERY_REQUIRED", "verification", "Unable to durably record verification failure");
578
+ }
579
+ }
580
+ }
581
+ async readVerifiedReport(session, expected) {
582
+ if (session.verification.status !== "passed") {
583
+ throw failure("RECOVERY_REQUIRED", "verification", "Passed verification state lacks immutable report evidence");
584
+ }
585
+ let receipt;
586
+ let bytes;
587
+ try {
588
+ receipt = VerificationReceiptSchema.parse(JSON.parse((await this.dependencies.store.readEvidence(session.id, GENERATION_BUNDLE_PATHS.verificationReceipt)).toString("utf8")));
589
+ bytes = await this.dependencies.store.readEvidence(session.id, receipt.report.path);
590
+ }
591
+ catch (error) {
592
+ throw failure("RECOVERY_REQUIRED", "verification", "Passed verification state lacks a valid immutable receipt", false, error);
593
+ }
594
+ if (!this.receiptMatches(receipt, expected)
595
+ || receipt.report.path !== session.verification.reportPath
596
+ || receipt.report.sha256 !== session.verification.reportSha256
597
+ || receipt.report.runId !== session.verification.runId
598
+ || sha256(bytes) !== receipt.report.sha256) {
599
+ throw failure("RECOVERY_REQUIRED", "verification", "Immutable verification receipt does not match durable state");
600
+ }
601
+ const report = TapHoundReportSchema.parse(JSON.parse(bytes.toString("utf8")));
602
+ if (report.runId !== receipt.report.runId) {
603
+ throw failure("RECOVERY_REQUIRED", "verification", "Immutable verification report is not a clean passed replay");
604
+ }
605
+ this.assertReport(report, {
606
+ version: 1,
607
+ name: expected.journey.name,
608
+ steps: session.candidateSteps
609
+ }, expected, "RECOVERY_REQUIRED");
610
+ return report;
611
+ }
612
+ buildMeta(session, outputPath, report) {
613
+ if (session.verification.status !== "passed") {
614
+ throw failure("RECOVERY_REQUIRED", "verification", "Generation verification is not durably passed");
615
+ }
616
+ return GenerationMetaSchema.parse({
617
+ version: 1,
618
+ status: "verified",
619
+ generationId: session.id,
620
+ journeyPath: outputPath,
621
+ bindings: {
622
+ projectHash: session.bindings.projectHash,
623
+ configHash: session.bindings.configHash,
624
+ contextHash: session.bindings.contextHash
625
+ },
626
+ verification: {
627
+ reportPath: GENERATION_BUNDLE_PATHS.verificationReport,
628
+ reportSha256: session.verification.reportSha256,
629
+ runId: report.runId,
630
+ runs: 1
631
+ },
632
+ manualOverrideStepIndexes: session.candidateSources.flatMap((source, index) => source === "manualOverride" ? [index] : [])
633
+ });
634
+ }
635
+ async ensureVerificationEvidence(generationId, path, expected) {
636
+ try {
637
+ await this.dependencies.store.writeTextEvidence(generationId, path, expected);
638
+ }
639
+ catch {
640
+ const existing = await this.dependencies.store.readEvidence(generationId, path);
641
+ if (!existing.equals(Buffer.from(expected))) {
642
+ throw failure("RECOVERY_REQUIRED", "verification", `Immutable verification evidence conflicts at ${path}`);
643
+ }
644
+ }
645
+ }
646
+ }