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,383 @@
1
+ import { GenerationConfirmationEvidenceSchema, GenerationSessionIdSchema, GenerationSessionSchema, hashGenerationConfirmationEvidence } from "../../domain/generation.js";
2
+ import { ProposedStepSchema, hashProposedStep } from "../../domain/proposed-step.js";
3
+ import { RuntimeSnapshotSchema, hashRuntimeSnapshot } from "../../domain/runtime-snapshot.js";
4
+ import { GenerationPromptCancelledError } from "../../ports/generation-prompt.js";
5
+ import { GenerationOperationError } from "./generation-starter.js";
6
+ import { ProposedStepValidator } from "./proposed-step-validator.js";
7
+ import { RiskEvaluator } from "./risk-evaluator.js";
8
+ function locatorSummary(step) {
9
+ if (step.action === "inputText"
10
+ || step.action === "back"
11
+ || step.action === "wait") {
12
+ return "";
13
+ }
14
+ return JSON.stringify(step.locator);
15
+ }
16
+ export function summarizeProposedStep(step) {
17
+ const before = step.activity.before;
18
+ if (step.action === "back") {
19
+ return `Back from ${before}`;
20
+ }
21
+ if (step.action === "wait") {
22
+ return `Wait on ${before}`;
23
+ }
24
+ if (step.action === "inputText") {
25
+ return `Input text on focused element in ${before}`;
26
+ }
27
+ if (step.action === "scrollTo") {
28
+ return `Scroll ${step.direction} in ${JSON.stringify(step.container)} to ${locatorSummary(step)} on ${before}`;
29
+ }
30
+ return `${step.action} ${locatorSummary(step)} on ${before}`;
31
+ }
32
+ function bindingFailure(message) {
33
+ return new GenerationOperationError("RISK_CONFIRMATION_REQUIRED", message);
34
+ }
35
+ export function confirmationEvidencePath(challengeId) {
36
+ return `evidence/confirmations/${GenerationSessionIdSchema.parse(challengeId)}.json`;
37
+ }
38
+ export class GenerationConfirmationService {
39
+ dependencies;
40
+ validator = new ProposedStepValidator();
41
+ riskEvaluator = new RiskEvaluator();
42
+ constructor(dependencies) {
43
+ this.dependencies = dependencies;
44
+ if (!Number.isSafeInteger(dependencies.confirmationTtlMs)
45
+ || dependencies.confirmationTtlMs <= 0) {
46
+ throw new Error("Confirmation TTL must be a positive safe integer");
47
+ }
48
+ }
49
+ request = async (input) => {
50
+ const session = GenerationSessionSchema.parse(await this.dependencies.store.read(input.generationId));
51
+ const submittedEvidence = GenerationConfirmationEvidenceSchema.parse({
52
+ version: 1,
53
+ proposal: input.proposal,
54
+ snapshot: input.snapshot,
55
+ source: input.source ?? "planner"
56
+ });
57
+ if (session.pendingConfirmation !== null
58
+ && session.pendingConfirmation.status === "pending") {
59
+ const storedEvidence = await this.readStoredEvidence({
60
+ generationId: session.id,
61
+ challengeId: session.pendingConfirmation.challengeId
62
+ });
63
+ if (JSON.stringify(storedEvidence) !== JSON.stringify(submittedEvidence)) {
64
+ throw bindingFailure("Pending generation confirmation does not match submitted evidence");
65
+ }
66
+ const challenge = this.requireExactPendingChallenge(session, storedEvidence);
67
+ return { status: "confirmationRequired", challenge };
68
+ }
69
+ const proposal = this.validator.validate({
70
+ session,
71
+ snapshot: submittedEvidence.snapshot,
72
+ proposal: submittedEvidence.proposal
73
+ });
74
+ const risk = this.riskEvaluator.evaluate(proposal.action, session.target.interactionPolicy);
75
+ if (risk.effectiveRisk === "forbidden") {
76
+ throw new GenerationOperationError("ACTION_FORBIDDEN", `${proposal.action} is forbidden by the Core interaction policy`);
77
+ }
78
+ if (risk.effectiveRisk === "safe") {
79
+ return { status: "approved", proposal };
80
+ }
81
+ if (session.revision === Number.MAX_SAFE_INTEGER) {
82
+ throw bindingFailure("Generation revision cannot create a challenge");
83
+ }
84
+ const evidence = GenerationConfirmationEvidenceSchema.parse({
85
+ version: 1,
86
+ proposal,
87
+ snapshot: submittedEvidence.snapshot,
88
+ source: submittedEvidence.source
89
+ });
90
+ const challenge = {
91
+ challengeId: this.dependencies.generateChallengeId(),
92
+ stepIndex: session.candidateSteps.length,
93
+ proposalHash: hashProposedStep(proposal),
94
+ snapshotHash: proposal.binding.snapshotHash,
95
+ evidenceHash: hashGenerationConfirmationEvidence(evidence),
96
+ actionSummary: summarizeProposedStep(proposal),
97
+ expiresAt: new Date(this.dependencies.now().getTime()
98
+ + this.dependencies.confirmationTtlMs).toISOString(),
99
+ status: "pending"
100
+ };
101
+ const next = GenerationSessionSchema.parse({
102
+ ...session,
103
+ revision: session.revision + 1,
104
+ pendingConfirmation: challenge
105
+ });
106
+ await this.dependencies.store.writeEvidence(session.id, confirmationEvidencePath(challenge.challengeId), evidence);
107
+ await this.updateConfirmationReconciled(session.id, session.revision, next);
108
+ return { status: "confirmationRequired", challenge };
109
+ };
110
+ requestManual = async (input) => {
111
+ const proposal = await this.dependencies.prompt.buildManualProposal(input.manual, input.signal);
112
+ if (input.signal?.aborted === true) {
113
+ throw new GenerationPromptCancelledError();
114
+ }
115
+ return this.request({
116
+ generationId: input.generationId,
117
+ proposal,
118
+ snapshot: input.snapshot,
119
+ source: "manualOverride"
120
+ });
121
+ };
122
+ findPendingManual = async (input) => {
123
+ const session = GenerationSessionSchema.parse(await this.dependencies.store.read(input.generationId));
124
+ const pending = session.pendingConfirmation;
125
+ if (pending === null || pending.status !== "pending") {
126
+ return null;
127
+ }
128
+ const evidence = await this.readStoredEvidence({
129
+ generationId: session.id,
130
+ challengeId: pending.challengeId
131
+ });
132
+ if (evidence.source !== "manualOverride"
133
+ || evidence.proposal.action !== input.action) {
134
+ throw bindingFailure("Pending manual confirmation does not match the requested action");
135
+ }
136
+ const challenge = this.requireExactPendingChallenge(session, evidence);
137
+ return { status: "confirmationRequired", challenge };
138
+ };
139
+ confirmStored = async (input) => {
140
+ const evidence = await this.readStoredEvidence(input);
141
+ await this.confirm({
142
+ generationId: input.generationId,
143
+ challengeId: input.challengeId,
144
+ proposal: evidence.proposal,
145
+ snapshot: evidence.snapshot,
146
+ source: evidence.source,
147
+ signal: input.signal
148
+ });
149
+ return {
150
+ status: "approved",
151
+ proposal: evidence.proposal,
152
+ snapshot: evidence.snapshot,
153
+ source: evidence.source
154
+ };
155
+ };
156
+ confirm = async (input) => {
157
+ const session = GenerationSessionSchema.parse(await this.dependencies.store.read(input.generationId));
158
+ const proposal = ProposedStepSchema.parse(input.proposal);
159
+ const snapshot = RuntimeSnapshotSchema.parse(input.snapshot);
160
+ const evidence = GenerationConfirmationEvidenceSchema.parse({
161
+ version: 1,
162
+ proposal,
163
+ snapshot,
164
+ source: input.source ?? "planner"
165
+ });
166
+ const evidenceHash = hashGenerationConfirmationEvidence(evidence);
167
+ const challenge = session.pendingConfirmation;
168
+ if (challenge === null) {
169
+ throw bindingFailure("No pending generation confirmation exists");
170
+ }
171
+ const expectedRevision = proposal.binding.baseRevision
172
+ + (challenge.status === "pending" ? 1 : 2);
173
+ const cleanupToken = {
174
+ expectedRevision,
175
+ expectedChallenge: challenge
176
+ };
177
+ const expired = this.dependencies.now().getTime()
178
+ >= new Date(challenge.expiresAt).getTime();
179
+ const challengeMatches = challenge.challengeId === input.challengeId;
180
+ const proposalMatches = challenge.proposalHash
181
+ === hashProposedStep(proposal);
182
+ const evidenceMatches = challenge.evidenceHash === evidenceHash;
183
+ const snapshotMatches = challenge.snapshotHash
184
+ === hashRuntimeSnapshot(snapshot)
185
+ && challenge.snapshotHash === proposal.binding.snapshotHash
186
+ && challenge.snapshotHash === session.bindings.snapshotHash;
187
+ const stateMatches = session.state === "active"
188
+ && session.inFlight === null
189
+ && session.verification.status === "notRun"
190
+ && session.publication.status === "notRun"
191
+ && session.revision === expectedRevision
192
+ && challenge.stepIndex === session.candidateSteps.length
193
+ && challenge.actionSummary === summarizeProposedStep(proposal);
194
+ if (expired
195
+ || !challengeMatches
196
+ || !proposalMatches
197
+ || !evidenceMatches
198
+ || !snapshotMatches
199
+ || !stateMatches) {
200
+ if (challengeMatches) {
201
+ await this.clearExactChallenge(session.id, cleanupToken);
202
+ }
203
+ if (expired) {
204
+ throw bindingFailure("Generation confirmation challenge expired");
205
+ }
206
+ if (!evidenceMatches) {
207
+ throw bindingFailure("Generation confirmation evidence hash is not authoritative");
208
+ }
209
+ throw bindingFailure("Generation confirmation binding is no longer authoritative");
210
+ }
211
+ try {
212
+ const validationSession = GenerationSessionSchema.parse({
213
+ ...session,
214
+ revision: proposal.binding.baseRevision,
215
+ pendingConfirmation: null
216
+ });
217
+ this.validator.validate({
218
+ session: validationSession,
219
+ snapshot,
220
+ proposal
221
+ });
222
+ }
223
+ catch {
224
+ await this.clearExactChallenge(session.id, cleanupToken);
225
+ throw bindingFailure("Generation proposal or runtime state changed before confirmation");
226
+ }
227
+ if (challenge.status === "approved") {
228
+ return { status: "approved", proposal };
229
+ }
230
+ let confirmed;
231
+ try {
232
+ confirmed = await this.dependencies.prompt.confirm(challenge, input.signal);
233
+ }
234
+ catch (error) {
235
+ await this.clearExactChallenge(session.id, cleanupToken);
236
+ throw error;
237
+ }
238
+ if (input.signal?.aborted === true) {
239
+ await this.cancelPrompt(session.id, cleanupToken);
240
+ }
241
+ if (!confirmed) {
242
+ await this.clearExactChallenge(session.id, cleanupToken);
243
+ throw bindingFailure("Generation action confirmation was declined");
244
+ }
245
+ const latest = GenerationSessionSchema.parse(await this.dependencies.store.read(input.generationId));
246
+ if (input.signal?.aborted === true) {
247
+ await this.cancelPrompt(session.id, cleanupToken);
248
+ }
249
+ const stillAuthoritative = latest.revision === session.revision
250
+ && JSON.stringify(latest.pendingConfirmation)
251
+ === JSON.stringify(challenge)
252
+ && this.dependencies.now().getTime()
253
+ < new Date(challenge.expiresAt).getTime();
254
+ if (!stillAuthoritative) {
255
+ await this.clearExactChallenge(session.id, cleanupToken);
256
+ throw bindingFailure("Generation confirmation state changed while prompting");
257
+ }
258
+ if (latest.revision === Number.MAX_SAFE_INTEGER) {
259
+ await this.clearExactChallenge(session.id, cleanupToken);
260
+ throw bindingFailure("Generation revision cannot approve challenge");
261
+ }
262
+ if (input.signal?.aborted === true) {
263
+ await this.cancelPrompt(session.id, cleanupToken);
264
+ }
265
+ const approved = GenerationSessionSchema.parse({
266
+ ...latest,
267
+ revision: latest.revision + 1,
268
+ pendingConfirmation: {
269
+ ...challenge,
270
+ status: "approved"
271
+ }
272
+ });
273
+ await this.dependencies.store.updateConfirmation(latest.id, latest.revision, approved);
274
+ return { status: "approved", proposal };
275
+ };
276
+ clearApproved = async (input) => {
277
+ const session = GenerationSessionSchema.parse(await this.dependencies.store.read(input.generationId));
278
+ const challenge = session.pendingConfirmation;
279
+ if (challenge === null
280
+ || JSON.stringify(challenge) !== JSON.stringify(input.challenge)
281
+ || challenge.status !== "approved") {
282
+ return;
283
+ }
284
+ await this.clearExactChallenge(session.id, {
285
+ expectedRevision: session.revision,
286
+ expectedChallenge: challenge
287
+ });
288
+ };
289
+ async clearExactChallenge(generationId, token) {
290
+ const latest = GenerationSessionSchema.parse(await this.dependencies.store.read(generationId));
291
+ const pending = latest.pendingConfirmation;
292
+ if (latest.revision !== token.expectedRevision
293
+ || pending === null
294
+ || JSON.stringify(pending) !== JSON.stringify(token.expectedChallenge)) {
295
+ return;
296
+ }
297
+ if (latest.revision === Number.MAX_SAFE_INTEGER) {
298
+ throw bindingFailure("Generation revision cannot clear challenge");
299
+ }
300
+ const cleared = GenerationSessionSchema.parse({
301
+ ...latest,
302
+ revision: latest.revision + 1,
303
+ pendingConfirmation: null
304
+ });
305
+ await this.updateConfirmationReconciled(latest.id, latest.revision, cleared);
306
+ }
307
+ async cancelPrompt(generationId, token) {
308
+ await this.clearExactChallenge(generationId, token);
309
+ throw new GenerationPromptCancelledError();
310
+ }
311
+ requireExactPendingChallenge(session, evidence) {
312
+ const challenge = session.pendingConfirmation;
313
+ const proposal = evidence.proposal;
314
+ const snapshot = evidence.snapshot;
315
+ if (challenge === null
316
+ || challenge.status !== "pending"
317
+ || session.revision !== proposal.binding.baseRevision + 1) {
318
+ throw bindingFailure("Pending generation confirmation is no longer authoritative");
319
+ }
320
+ try {
321
+ const baseSession = GenerationSessionSchema.parse({
322
+ ...session,
323
+ revision: proposal.binding.baseRevision,
324
+ pendingConfirmation: null
325
+ });
326
+ this.validator.validate({
327
+ session: baseSession,
328
+ snapshot,
329
+ proposal
330
+ });
331
+ }
332
+ catch {
333
+ throw bindingFailure("Pending generation confirmation is no longer authoritative");
334
+ }
335
+ const risk = this.riskEvaluator.evaluate(proposal.action, session.target.interactionPolicy);
336
+ if (risk.effectiveRisk !== "confirmationRequired"
337
+ || challenge.stepIndex !== session.candidateSteps.length
338
+ || challenge.proposalHash !== hashProposedStep(proposal)
339
+ || challenge.snapshotHash !== hashRuntimeSnapshot(snapshot)
340
+ || challenge.snapshotHash !== proposal.binding.snapshotHash
341
+ || challenge.evidenceHash !== hashGenerationConfirmationEvidence(evidence)
342
+ || challenge.actionSummary !== summarizeProposedStep(proposal)
343
+ || this.dependencies.now().getTime()
344
+ >= new Date(challenge.expiresAt).getTime()) {
345
+ throw bindingFailure("Pending generation confirmation does not match stored evidence");
346
+ }
347
+ return challenge;
348
+ }
349
+ async updateConfirmationReconciled(generationId, expectedRevision, intended) {
350
+ try {
351
+ await this.dependencies.store.updateConfirmation(generationId, expectedRevision, intended);
352
+ }
353
+ catch (error) {
354
+ let authoritative;
355
+ try {
356
+ authoritative = GenerationSessionSchema.parse(await this.dependencies.store.read(generationId));
357
+ }
358
+ catch {
359
+ throw error;
360
+ }
361
+ if (JSON.stringify(authoritative) === JSON.stringify(intended)) {
362
+ return;
363
+ }
364
+ throw error;
365
+ }
366
+ }
367
+ async readStoredEvidence(input) {
368
+ try {
369
+ const parsed = JSON.parse((await this.dependencies.store.readEvidence(input.generationId, confirmationEvidencePath(input.challengeId))).toString("utf8"));
370
+ const evidence = GenerationConfirmationEvidenceSchema.parse(parsed);
371
+ if (evidence.proposal.binding.generationId !== input.generationId
372
+ || evidence.snapshot.generationId !== input.generationId) {
373
+ throw new Error("Generation confirmation evidence belongs to another session");
374
+ }
375
+ return evidence;
376
+ }
377
+ catch (error) {
378
+ throw bindingFailure(error instanceof Error
379
+ ? `Generation confirmation evidence is unavailable: ${error.message}`
380
+ : "Generation confirmation evidence is unavailable");
381
+ }
382
+ }
383
+ }
@@ -0,0 +1,67 @@
1
+ import { z } from "zod";
2
+ import { type GenerationErrorCode, type GenerationMeta } from "../../domain/generation.js";
3
+ import { type Journey } from "../../domain/journey.js";
4
+ import { type ProjectContext } from "../../domain/project-context.js";
5
+ import { type TapHoundConfig } from "../../domain/config.js";
6
+ import type { AdbPort } from "../../ports/adb.js";
7
+ import { type GenerationSessionStore } from "../../ports/generation-session-store.js";
8
+ import type { ContextValidator } from "../context/context-validator.js";
9
+ import type { ProjectDescription } from "../project/project-describer.js";
10
+ import type { VerifyRuntime } from "../runtime/verify-runtime.js";
11
+ import { GenerationPublisher } from "./generation-publisher.js";
12
+ export declare const GenerationOutputPathSchema: z.ZodPipe<z.ZodString, z.ZodTransform<string, string>>;
13
+ export type GenerationFinalizationStage = "precondition" | "verification" | "publication" | "export";
14
+ export declare class GenerationFinalizationError extends Error {
15
+ readonly code: GenerationErrorCode;
16
+ readonly stage: GenerationFinalizationStage;
17
+ readonly recoverable: boolean;
18
+ readonly name = "GenerationFinalizationError";
19
+ constructor(code: GenerationErrorCode, stage: GenerationFinalizationStage, recoverable: boolean, message: string, options?: ErrorOptions);
20
+ }
21
+ export interface GenerationFinalizeInput {
22
+ generationId: string;
23
+ projectRoot: string;
24
+ config: TapHoundConfig;
25
+ context: ProjectContext;
26
+ project: ProjectDescription;
27
+ outputPath: string;
28
+ name?: string | undefined;
29
+ deviceSerial: string;
30
+ toolVersions: Record<string, string>;
31
+ signal?: AbortSignal | undefined;
32
+ }
33
+ export interface GenerationFinalizerDependencies {
34
+ store: Pick<GenerationSessionStore, "read" | "beginVerification" | "completeVerification" | "failVerification" | "markBundlePublishable" | "writeTextEvidence" | "readEvidence">;
35
+ contextValidator: Pick<ContextValidator, "validate">;
36
+ adb: Pick<AdbPort, "forceStop">;
37
+ verifyRuntime: Pick<VerifyRuntime, "verify">;
38
+ publisher: GenerationPublisher;
39
+ generateAttemptId: () => string;
40
+ }
41
+ export interface GenerationFinalizeResult {
42
+ status: "verified";
43
+ journey: Journey;
44
+ meta: GenerationMeta;
45
+ bundlePath: string;
46
+ journeyPath: string;
47
+ metaPath: string;
48
+ replayed: boolean;
49
+ }
50
+ export declare class GenerationFinalizer {
51
+ private readonly dependencies;
52
+ constructor(dependencies: GenerationFinalizerDependencies);
53
+ readonly finalize: (input: GenerationFinalizeInput) => Promise<GenerationFinalizeResult>;
54
+ private assertBindings;
55
+ private revalidate;
56
+ private beginVerification;
57
+ private expectedVerification;
58
+ private assertEligible;
59
+ private assertReport;
60
+ private receiptMatches;
61
+ private persistPassedVerification;
62
+ private reconcilePassedVerification;
63
+ private persistFailedVerification;
64
+ private readVerifiedReport;
65
+ private buildMeta;
66
+ private ensureVerificationEvidence;
67
+ }