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.
- package/LICENSE +202 -0
- package/README.md +200 -0
- package/README.zh-CN.md +200 -0
- package/assets/brand/taphound-mark.svg +9 -0
- package/assets/skills/taphound-ai-journey/GUIDE.md +862 -0
- package/assets/skills/taphound-ai-journey/SKILL.md +377 -0
- package/assets/skills/taphound-ai-journey/prompts/analyze-project.md +526 -0
- package/assets/skills/taphound-ai-journey/prompts/check-completion.md +52 -0
- package/assets/skills/taphound-ai-journey/prompts/generate-step.md +109 -0
- package/assets/skills/taphound-ai-journey/schemas/observe-output.json +42 -0
- package/assets/skills/taphound-ai-journey/schemas/project-context.json +81 -0
- package/assets/skills/taphound-ai-journey/schemas/proposed-step-envelope.json +267 -0
- package/assets/skills/taphound-ai-journey/templates/project-context.example.json +49 -0
- package/dist/adapters/adb/adb-adapter.d.ts +23 -0
- package/dist/adapters/adb/adb-adapter.js +218 -0
- package/dist/adapters/android-cli/android-cli-adapter.d.ts +11 -0
- package/dist/adapters/android-cli/android-cli-adapter.js +64 -0
- package/dist/adapters/android-cli/layout-parser.d.ts +3 -0
- package/dist/adapters/android-cli/layout-parser.js +174 -0
- package/dist/adapters/clock/system-clock.d.ts +5 -0
- package/dist/adapters/clock/system-clock.js +13 -0
- package/dist/adapters/filesystem/artifact-store.d.ts +4 -0
- package/dist/adapters/filesystem/artifact-store.js +73 -0
- package/dist/adapters/filesystem/generation-meta-writer.d.ts +16 -0
- package/dist/adapters/filesystem/generation-meta-writer.js +34 -0
- package/dist/adapters/filesystem/generation-session-store.d.ts +68 -0
- package/dist/adapters/filesystem/generation-session-store.js +1603 -0
- package/dist/adapters/filesystem/journey-writer.d.ts +16 -0
- package/dist/adapters/filesystem/journey-writer.js +34 -0
- package/dist/adapters/filesystem/project-bound-file.d.ts +6 -0
- package/dist/adapters/filesystem/project-bound-file.js +240 -0
- package/dist/adapters/filesystem/project-file-inspector.d.ts +8 -0
- package/dist/adapters/filesystem/project-file-inspector.js +198 -0
- package/dist/adapters/filesystem/skill-installer.d.ts +10 -0
- package/dist/adapters/filesystem/skill-installer.js +39 -0
- package/dist/adapters/process/node-process-runner.d.ts +8 -0
- package/dist/adapters/process/node-process-runner.js +155 -0
- package/dist/adapters/prompt/inquirer-generation-prompt.d.ts +55 -0
- package/dist/adapters/prompt/inquirer-generation-prompt.js +213 -0
- package/dist/adapters/prompt/inquirer-init-prompt.d.ts +32 -0
- package/dist/adapters/prompt/inquirer-init-prompt.js +55 -0
- package/dist/adapters/prompt/inquirer-recorder-prompt.d.ts +47 -0
- package/dist/adapters/prompt/inquirer-recorder-prompt.js +157 -0
- package/dist/application/assertion/expectation-evaluator.d.ts +61 -0
- package/dist/application/assertion/expectation-evaluator.js +207 -0
- package/dist/application/collector/logcat-collector.d.ts +39 -0
- package/dist/application/collector/logcat-collector.js +107 -0
- package/dist/application/context/context-validator.d.ts +27 -0
- package/dist/application/context/context-validator.js +120 -0
- package/dist/application/doctor/doctor-service.d.ts +35 -0
- package/dist/application/doctor/doctor-service.js +178 -0
- package/dist/application/generation/focused-input.d.ts +2 -0
- package/dist/application/generation/focused-input.js +9 -0
- package/dist/application/generation/generation-confirmation-service.d.ts +76 -0
- package/dist/application/generation/generation-confirmation-service.js +383 -0
- package/dist/application/generation/generation-finalizer.d.ts +67 -0
- package/dist/application/generation/generation-finalizer.js +646 -0
- package/dist/application/generation/generation-publisher.d.ts +46 -0
- package/dist/application/generation/generation-publisher.js +178 -0
- package/dist/application/generation/generation-starter.d.ts +31 -0
- package/dist/application/generation/generation-starter.js +102 -0
- package/dist/application/generation/generation-step-executor.d.ts +52 -0
- package/dist/application/generation/generation-step-executor.js +668 -0
- package/dist/application/generation/proposed-step-validator.d.ts +11 -0
- package/dist/application/generation/proposed-step-validator.js +88 -0
- package/dist/application/generation/risk-evaluator.d.ts +9 -0
- package/dist/application/generation/risk-evaluator.js +14 -0
- package/dist/application/generation/runtime-observer.d.ts +39 -0
- package/dist/application/generation/runtime-observer.js +157 -0
- package/dist/application/init/init-service.d.ts +17 -0
- package/dist/application/init/init-service.js +44 -0
- package/dist/application/interaction/action-executor.d.ts +25 -0
- package/dist/application/interaction/action-executor.js +103 -0
- package/dist/application/interaction/fallback-resolver.d.ts +24 -0
- package/dist/application/interaction/fallback-resolver.js +48 -0
- package/dist/application/interaction/scroll-to-executor.d.ts +40 -0
- package/dist/application/interaction/scroll-to-executor.js +191 -0
- package/dist/application/locator/locator-resolver.d.ts +20 -0
- package/dist/application/locator/locator-resolver.js +89 -0
- package/dist/application/project/project-describer.d.ts +14 -0
- package/dist/application/project/project-describer.js +8 -0
- package/dist/application/recorder/locator-selector.d.ts +10 -0
- package/dist/application/recorder/locator-selector.js +80 -0
- package/dist/application/recorder/recorder-service.d.ts +41 -0
- package/dist/application/recorder/recorder-service.js +331 -0
- package/dist/application/report/report-writer.d.ts +10 -0
- package/dist/application/report/report-writer.js +39 -0
- package/dist/application/runtime/activity-waiter.d.ts +33 -0
- package/dist/application/runtime/activity-waiter.js +73 -0
- package/dist/application/runtime/launch-failure.d.ts +2 -0
- package/dist/application/runtime/launch-failure.js +20 -0
- package/dist/application/runtime/process-waiter.d.ts +27 -0
- package/dist/application/runtime/process-waiter.js +73 -0
- package/dist/application/runtime/step-runner.d.ts +50 -0
- package/dist/application/runtime/step-runner.js +429 -0
- package/dist/application/runtime/verify-runtime.d.ts +44 -0
- package/dist/application/runtime/verify-runtime.js +350 -0
- package/dist/application/wait/idle-waiter.d.ts +30 -0
- package/dist/application/wait/idle-waiter.js +90 -0
- package/dist/cli/commands/context.d.ts +3 -0
- package/dist/cli/commands/context.js +94 -0
- package/dist/cli/commands/doctor.d.ts +3 -0
- package/dist/cli/commands/doctor.js +51 -0
- package/dist/cli/commands/generation.d.ts +3 -0
- package/dist/cli/commands/generation.js +518 -0
- package/dist/cli/commands/init.d.ts +3 -0
- package/dist/cli/commands/init.js +104 -0
- package/dist/cli/commands/project.d.ts +3 -0
- package/dist/cli/commands/project.js +58 -0
- package/dist/cli/commands/record.d.ts +3 -0
- package/dist/cli/commands/record.js +88 -0
- package/dist/cli/commands/verify.d.ts +3 -0
- package/dist/cli/commands/verify.js +106 -0
- package/dist/cli/dependencies.d.ts +66 -0
- package/dist/cli/dependencies.js +204 -0
- package/dist/cli/main.d.ts +8 -0
- package/dist/cli/main.js +73 -0
- package/dist/cli/output.d.ts +16 -0
- package/dist/cli/output.js +22 -0
- package/dist/cli/program.d.ts +3 -0
- package/dist/cli/program.js +43 -0
- package/dist/domain/activity.d.ts +7 -0
- package/dist/domain/activity.js +65 -0
- package/dist/domain/app-process.d.ts +7 -0
- package/dist/domain/app-process.js +10 -0
- package/dist/domain/config.d.ts +15 -0
- package/dist/domain/config.js +17 -0
- package/dist/domain/failure.d.ts +4 -0
- package/dist/domain/failure.js +43 -0
- package/dist/domain/generation.d.ts +912 -0
- package/dist/domain/generation.js +342 -0
- package/dist/domain/init.d.ts +42 -0
- package/dist/domain/init.js +114 -0
- package/dist/domain/journey.d.ts +673 -0
- package/dist/domain/journey.js +109 -0
- package/dist/domain/layout.d.ts +35 -0
- package/dist/domain/layout.js +33 -0
- package/dist/domain/locator.d.ts +8 -0
- package/dist/domain/locator.js +5 -0
- package/dist/domain/project-context.d.ts +101 -0
- package/dist/domain/project-context.js +64 -0
- package/dist/domain/proposed-step.d.ts +341 -0
- package/dist/domain/proposed-step.js +87 -0
- package/dist/domain/report.d.ts +349 -0
- package/dist/domain/report.js +135 -0
- package/dist/domain/runtime-snapshot.d.ts +16 -0
- package/dist/domain/runtime-snapshot.js +46 -0
- package/dist/ports/adb.d.ts +42 -0
- package/dist/ports/adb.js +1 -0
- package/dist/ports/android-cli.d.ts +21 -0
- package/dist/ports/android-cli.js +1 -0
- package/dist/ports/artifact-store.d.ts +12 -0
- package/dist/ports/artifact-store.js +1 -0
- package/dist/ports/clock.d.ts +4 -0
- package/dist/ports/clock.js +1 -0
- package/dist/ports/generation-meta-writer.d.ts +10 -0
- package/dist/ports/generation-meta-writer.js +1 -0
- package/dist/ports/generation-prompt.d.ts +19 -0
- package/dist/ports/generation-prompt.js +6 -0
- package/dist/ports/generation-session-store.d.ts +32 -0
- package/dist/ports/generation-session-store.js +26 -0
- package/dist/ports/init-prompt.d.ts +8 -0
- package/dist/ports/init-prompt.js +6 -0
- package/dist/ports/journey-writer.d.ts +10 -0
- package/dist/ports/journey-writer.js +1 -0
- package/dist/ports/process-runner.d.ts +32 -0
- package/dist/ports/process-runner.js +1 -0
- package/dist/ports/project-bound-file.d.ts +8 -0
- package/dist/ports/project-bound-file.js +1 -0
- package/dist/ports/project-file-inspector.d.ts +33 -0
- package/dist/ports/project-file-inspector.js +1 -0
- package/dist/ports/recorder-prompt.d.ts +29 -0
- package/dist/ports/recorder-prompt.js +1 -0
- package/dist/ports/skill-installer.d.ts +8 -0
- package/dist/ports/skill-installer.js +1 -0
- package/package.json +64 -0
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { type GenerationBundleManifest, type GenerationMeta, type GenerationReport } from "../../domain/generation.js";
|
|
2
|
+
import { type Journey } from "../../domain/journey.js";
|
|
3
|
+
import { type TapHoundReport } from "../../domain/report.js";
|
|
4
|
+
import type { ProjectBoundGenerationMetaWriterPort } from "../../ports/generation-meta-writer.js";
|
|
5
|
+
import { type GenerationSessionStore } from "../../ports/generation-session-store.js";
|
|
6
|
+
import type { ProjectBoundJourneyWriterPort } from "../../ports/journey-writer.js";
|
|
7
|
+
export declare const GENERATION_BUNDLE_PATHS: {
|
|
8
|
+
readonly candidateJourney: "candidate/journey.json";
|
|
9
|
+
readonly verifiedJourney: "verified/journey.json";
|
|
10
|
+
readonly generationReport: "generation-report.json";
|
|
11
|
+
readonly verificationReport: "verification/report.json";
|
|
12
|
+
readonly verificationReceipt: "verification/receipt.json";
|
|
13
|
+
readonly meta: "meta.json";
|
|
14
|
+
readonly manifest: "manifest.json";
|
|
15
|
+
};
|
|
16
|
+
export declare function generationMetaOutputPath(journeyOutputPath: string): string;
|
|
17
|
+
export interface GenerationBundleContent {
|
|
18
|
+
generationId: string;
|
|
19
|
+
journey: Journey;
|
|
20
|
+
report: GenerationReport;
|
|
21
|
+
verificationReport: TapHoundReport;
|
|
22
|
+
meta: GenerationMeta;
|
|
23
|
+
}
|
|
24
|
+
export interface GenerationPublisherDependencies {
|
|
25
|
+
store: Pick<GenerationSessionStore, "writeTextEvidence" | "readEvidence" | "listEvidence" | "publish">;
|
|
26
|
+
journeyWriter: ProjectBoundJourneyWriterPort;
|
|
27
|
+
metaWriter: ProjectBoundGenerationMetaWriterPort;
|
|
28
|
+
}
|
|
29
|
+
export declare class GenerationPublisher {
|
|
30
|
+
private readonly dependencies;
|
|
31
|
+
constructor(dependencies: GenerationPublisherDependencies);
|
|
32
|
+
readonly stage: (input: GenerationBundleContent) => Promise<GenerationBundleManifest>;
|
|
33
|
+
readonly publish: (generationId: string) => Promise<string>;
|
|
34
|
+
readonly export: (input: {
|
|
35
|
+
generationId: string;
|
|
36
|
+
projectRoot: string;
|
|
37
|
+
journeyPath: string;
|
|
38
|
+
journey: Journey;
|
|
39
|
+
meta: GenerationMeta;
|
|
40
|
+
}) => Promise<{
|
|
41
|
+
journeyPath: string;
|
|
42
|
+
metaPath: string;
|
|
43
|
+
}>;
|
|
44
|
+
private readonly verifyPublishedManifest;
|
|
45
|
+
private readonly verifyManifest;
|
|
46
|
+
}
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { extname, resolve } from "node:path";
|
|
3
|
+
import { GenerationBundleManifestSchema, GenerationMetaSchema, GenerationReportSchema } from "../../domain/generation.js";
|
|
4
|
+
import { JourneySchema } from "../../domain/journey.js";
|
|
5
|
+
import { TapHoundReportSchema } from "../../domain/report.js";
|
|
6
|
+
import { GenerationSessionStoreError } from "../../ports/generation-session-store.js";
|
|
7
|
+
export const GENERATION_BUNDLE_PATHS = {
|
|
8
|
+
candidateJourney: "candidate/journey.json",
|
|
9
|
+
verifiedJourney: "verified/journey.json",
|
|
10
|
+
generationReport: "generation-report.json",
|
|
11
|
+
verificationReport: "verification/report.json",
|
|
12
|
+
verificationReceipt: "verification/receipt.json",
|
|
13
|
+
meta: "meta.json",
|
|
14
|
+
manifest: "manifest.json"
|
|
15
|
+
};
|
|
16
|
+
function serializeJson(value) {
|
|
17
|
+
return `${JSON.stringify(value, null, 2)}\n`;
|
|
18
|
+
}
|
|
19
|
+
function sha256(bytes) {
|
|
20
|
+
return createHash("sha256").update(bytes).digest("hex");
|
|
21
|
+
}
|
|
22
|
+
function manifestEntry(path, bytes) {
|
|
23
|
+
return {
|
|
24
|
+
path,
|
|
25
|
+
bytes: Buffer.byteLength(bytes),
|
|
26
|
+
sha256: sha256(bytes)
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
async function ensureEvidence(store, generationId, path, expected) {
|
|
30
|
+
try {
|
|
31
|
+
await store.writeTextEvidence(generationId, path, expected);
|
|
32
|
+
}
|
|
33
|
+
catch (error) {
|
|
34
|
+
if (!(error instanceof GenerationSessionStoreError)
|
|
35
|
+
|| error.code !== "EVIDENCE_ALREADY_EXISTS") {
|
|
36
|
+
throw error;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
const stored = await store.readEvidence(generationId, path);
|
|
40
|
+
if (!stored.equals(Buffer.from(expected))) {
|
|
41
|
+
throw new GenerationSessionStoreError("INVALID_EVIDENCE", `Immutable evidence does not match expected bytes: ${path}`);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
export function generationMetaOutputPath(journeyOutputPath) {
|
|
45
|
+
const extension = extname(journeyOutputPath);
|
|
46
|
+
return extension.toLowerCase() === ".json"
|
|
47
|
+
? `${journeyOutputPath.slice(0, -extension.length)}.meta.json`
|
|
48
|
+
: `${journeyOutputPath}.meta.json`;
|
|
49
|
+
}
|
|
50
|
+
export class GenerationPublisher {
|
|
51
|
+
dependencies;
|
|
52
|
+
constructor(dependencies) {
|
|
53
|
+
this.dependencies = dependencies;
|
|
54
|
+
}
|
|
55
|
+
stage = async (input) => {
|
|
56
|
+
const journey = JourneySchema.parse(input.journey);
|
|
57
|
+
const report = GenerationReportSchema.parse(input.report);
|
|
58
|
+
const verificationReport = TapHoundReportSchema.parse(input.verificationReport);
|
|
59
|
+
const meta = GenerationMetaSchema.parse(input.meta);
|
|
60
|
+
if (input.generationId !== report.generationId
|
|
61
|
+
|| input.generationId !== meta.generationId
|
|
62
|
+
|| report.steps.length !== journey.steps.length) {
|
|
63
|
+
throw new Error("Generation bundle identities do not align");
|
|
64
|
+
}
|
|
65
|
+
const journeyBytes = serializeJson(journey);
|
|
66
|
+
const receiptBytes = (await this.dependencies.store.readEvidence(input.generationId, GENERATION_BUNDLE_PATHS.verificationReceipt)).toString("utf8");
|
|
67
|
+
const files = [
|
|
68
|
+
[GENERATION_BUNDLE_PATHS.candidateJourney, journeyBytes],
|
|
69
|
+
[GENERATION_BUNDLE_PATHS.verifiedJourney, journeyBytes],
|
|
70
|
+
[GENERATION_BUNDLE_PATHS.generationReport, serializeJson(report)],
|
|
71
|
+
[
|
|
72
|
+
GENERATION_BUNDLE_PATHS.verificationReport,
|
|
73
|
+
serializeJson(verificationReport)
|
|
74
|
+
],
|
|
75
|
+
[GENERATION_BUNDLE_PATHS.verificationReceipt, receiptBytes],
|
|
76
|
+
[GENERATION_BUNDLE_PATHS.meta, serializeJson(meta)]
|
|
77
|
+
];
|
|
78
|
+
for (const [path, bytes] of files) {
|
|
79
|
+
await ensureEvidence(this.dependencies.store, input.generationId, path, bytes);
|
|
80
|
+
}
|
|
81
|
+
const evidenceFiles = await this.dependencies.store.listEvidence(input.generationId);
|
|
82
|
+
const manifest = GenerationBundleManifestSchema.parse({
|
|
83
|
+
version: 1,
|
|
84
|
+
generationId: input.generationId,
|
|
85
|
+
files: evidenceFiles.map((file) => manifestEntry(file.path, Buffer.from(file.contentBase64, "base64")))
|
|
86
|
+
});
|
|
87
|
+
await ensureEvidence(this.dependencies.store, input.generationId, GENERATION_BUNDLE_PATHS.manifest, serializeJson(manifest));
|
|
88
|
+
await this.verifyManifest(input.generationId, manifest);
|
|
89
|
+
return manifest;
|
|
90
|
+
};
|
|
91
|
+
publish = async (generationId) => {
|
|
92
|
+
await this.verifyPublishedManifest(generationId);
|
|
93
|
+
const bundlePath = await this.dependencies.store.publish(generationId);
|
|
94
|
+
await this.verifyPublishedManifest(generationId);
|
|
95
|
+
return bundlePath;
|
|
96
|
+
};
|
|
97
|
+
export = async (input) => {
|
|
98
|
+
const journey = JourneySchema.parse(input.journey);
|
|
99
|
+
const meta = GenerationMetaSchema.parse(input.meta);
|
|
100
|
+
const journeyPath = resolve(input.projectRoot, input.journeyPath);
|
|
101
|
+
const metaPath = generationMetaOutputPath(journeyPath);
|
|
102
|
+
const authorityRoot = resolve(input.projectRoot, ".taphound");
|
|
103
|
+
try {
|
|
104
|
+
await this.dependencies.journeyWriter.writeProjectBound({
|
|
105
|
+
projectRoot: input.projectRoot,
|
|
106
|
+
authorityRoot,
|
|
107
|
+
outputPath: journeyPath,
|
|
108
|
+
journey
|
|
109
|
+
});
|
|
110
|
+
const expectedJourney = Buffer.from(serializeJson(journey));
|
|
111
|
+
const actualJourney = await this.dependencies.journeyWriter
|
|
112
|
+
.readProjectBound({
|
|
113
|
+
projectRoot: input.projectRoot,
|
|
114
|
+
authorityRoot,
|
|
115
|
+
outputPath: journeyPath
|
|
116
|
+
});
|
|
117
|
+
if (!actualJourney.equals(expectedJourney)) {
|
|
118
|
+
throw new Error("Exported Journey bytes do not match verified evidence");
|
|
119
|
+
}
|
|
120
|
+
await this.verifyPublishedManifest(input.generationId);
|
|
121
|
+
}
|
|
122
|
+
catch (error) {
|
|
123
|
+
await this.verifyPublishedManifest(input.generationId);
|
|
124
|
+
throw error;
|
|
125
|
+
}
|
|
126
|
+
try {
|
|
127
|
+
await this.dependencies.metaWriter.writeProjectBound({
|
|
128
|
+
projectRoot: input.projectRoot,
|
|
129
|
+
authorityRoot,
|
|
130
|
+
outputPath: metaPath,
|
|
131
|
+
meta
|
|
132
|
+
});
|
|
133
|
+
const expectedMeta = Buffer.from(serializeJson(meta));
|
|
134
|
+
const actualMeta = await this.dependencies.metaWriter.readProjectBound({
|
|
135
|
+
projectRoot: input.projectRoot,
|
|
136
|
+
authorityRoot,
|
|
137
|
+
outputPath: metaPath
|
|
138
|
+
});
|
|
139
|
+
if (!actualMeta.equals(expectedMeta)) {
|
|
140
|
+
throw new Error("Exported generation meta bytes do not match evidence");
|
|
141
|
+
}
|
|
142
|
+
await this.verifyPublishedManifest(input.generationId);
|
|
143
|
+
}
|
|
144
|
+
catch (error) {
|
|
145
|
+
await this.verifyPublishedManifest(input.generationId);
|
|
146
|
+
throw error;
|
|
147
|
+
}
|
|
148
|
+
return { journeyPath, metaPath };
|
|
149
|
+
};
|
|
150
|
+
verifyPublishedManifest = async (generationId) => {
|
|
151
|
+
const manifest = GenerationBundleManifestSchema.parse(JSON.parse((await this.dependencies.store.readEvidence(generationId, GENERATION_BUNDLE_PATHS.manifest)).toString("utf8")));
|
|
152
|
+
if (manifest.generationId !== generationId) {
|
|
153
|
+
throw new GenerationSessionStoreError("INVALID_EVIDENCE", "Published manifest generation id does not match");
|
|
154
|
+
}
|
|
155
|
+
await this.verifyManifest(generationId, manifest);
|
|
156
|
+
};
|
|
157
|
+
verifyManifest = async (generationId, manifest) => {
|
|
158
|
+
GenerationBundleManifestSchema.parse(manifest);
|
|
159
|
+
const evidenceFiles = await this.dependencies.store.listEvidence(generationId);
|
|
160
|
+
const manifestPaths = manifest.files.map((file) => file.path);
|
|
161
|
+
const evidencePaths = evidenceFiles.map((file) => file.path);
|
|
162
|
+
if (JSON.stringify(manifestPaths) !== JSON.stringify(evidencePaths)) {
|
|
163
|
+
throw new GenerationSessionStoreError("INVALID_EVIDENCE", "Manifest does not exactly enumerate immutable generation evidence");
|
|
164
|
+
}
|
|
165
|
+
for (const file of manifest.files) {
|
|
166
|
+
const evidence = evidenceFiles.find((candidate) => candidate.path === file.path);
|
|
167
|
+
if (evidence === undefined) {
|
|
168
|
+
throw new GenerationSessionStoreError("INVALID_EVIDENCE", `Manifest content is missing: ${file.path}`);
|
|
169
|
+
}
|
|
170
|
+
const bytes = Buffer.from(evidence.contentBase64, "base64");
|
|
171
|
+
if (bytes.byteLength !== evidence.byteLength
|
|
172
|
+
|| bytes.byteLength !== file.bytes
|
|
173
|
+
|| sha256(bytes) !== file.sha256) {
|
|
174
|
+
throw new GenerationSessionStoreError("INVALID_EVIDENCE", `Manifest content verification failed: ${file.path}`);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
};
|
|
178
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { ContextValidator } from "../context/context-validator.js";
|
|
2
|
+
import type { ProjectDescription } from "../project/project-describer.js";
|
|
3
|
+
import { type GenerationErrorCode, type GenerationSession } from "../../domain/generation.js";
|
|
4
|
+
import { type ProjectContext } from "../../domain/project-context.js";
|
|
5
|
+
import { type TapHoundConfig } from "../../domain/config.js";
|
|
6
|
+
import type { GenerationSessionStore } from "../../ports/generation-session-store.js";
|
|
7
|
+
export declare class GenerationOperationError extends Error {
|
|
8
|
+
readonly code: GenerationErrorCode;
|
|
9
|
+
readonly name = "GenerationOperationError";
|
|
10
|
+
constructor(code: GenerationErrorCode, message: string);
|
|
11
|
+
}
|
|
12
|
+
export interface GenerationStartInput {
|
|
13
|
+
projectRoot: string;
|
|
14
|
+
config: TapHoundConfig;
|
|
15
|
+
context: ProjectContext;
|
|
16
|
+
project: ProjectDescription;
|
|
17
|
+
deviceSerial: string;
|
|
18
|
+
}
|
|
19
|
+
export interface GenerationStarterDependencies {
|
|
20
|
+
contextValidator: Pick<ContextValidator, "validate">;
|
|
21
|
+
store: Pick<GenerationSessionStore, "create">;
|
|
22
|
+
now: () => Date;
|
|
23
|
+
generateId: () => string;
|
|
24
|
+
randomBytes: (size: number) => Uint8Array;
|
|
25
|
+
}
|
|
26
|
+
export declare function hashGenerationBinding(value: unknown): string;
|
|
27
|
+
export declare class GenerationStarter {
|
|
28
|
+
private readonly dependencies;
|
|
29
|
+
constructor(dependencies: GenerationStarterDependencies);
|
|
30
|
+
readonly start: (input: GenerationStartInput) => Promise<GenerationSession>;
|
|
31
|
+
}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { GenerationSessionSchema } from "../../domain/generation.js";
|
|
3
|
+
import { ProjectContextSchema } from "../../domain/project-context.js";
|
|
4
|
+
import { TapHoundConfigSchema } from "../../domain/config.js";
|
|
5
|
+
export class GenerationOperationError extends Error {
|
|
6
|
+
code;
|
|
7
|
+
name = "GenerationOperationError";
|
|
8
|
+
constructor(code, message) {
|
|
9
|
+
super(message);
|
|
10
|
+
this.code = code;
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
function canonicalize(value) {
|
|
14
|
+
if (Array.isArray(value)) {
|
|
15
|
+
return value.map(canonicalize);
|
|
16
|
+
}
|
|
17
|
+
if (value !== null && typeof value === "object") {
|
|
18
|
+
return Object.fromEntries(Object.entries(value)
|
|
19
|
+
.filter(([, item]) => item !== undefined)
|
|
20
|
+
.sort(([left], [right]) => left.localeCompare(right))
|
|
21
|
+
.map(([key, item]) => [key, canonicalize(item)]));
|
|
22
|
+
}
|
|
23
|
+
return value;
|
|
24
|
+
}
|
|
25
|
+
export function hashGenerationBinding(value) {
|
|
26
|
+
return createHash("sha256")
|
|
27
|
+
.update(JSON.stringify(canonicalize(value)))
|
|
28
|
+
.digest("hex");
|
|
29
|
+
}
|
|
30
|
+
function randomHex(bytes) {
|
|
31
|
+
return Buffer.from(bytes).toString("hex");
|
|
32
|
+
}
|
|
33
|
+
function distinctId(generationId, generateId) {
|
|
34
|
+
for (let attempt = 0; attempt < 32; attempt += 1) {
|
|
35
|
+
const candidate = generateId();
|
|
36
|
+
if (candidate !== generationId) {
|
|
37
|
+
return candidate;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
throw new Error("Unable to generate a distinct Journey run ID");
|
|
41
|
+
}
|
|
42
|
+
export class GenerationStarter {
|
|
43
|
+
dependencies;
|
|
44
|
+
constructor(dependencies) {
|
|
45
|
+
this.dependencies = dependencies;
|
|
46
|
+
}
|
|
47
|
+
start = async (input) => {
|
|
48
|
+
const config = TapHoundConfigSchema.parse(input.config);
|
|
49
|
+
if (input.project.projectRoot !== input.projectRoot) {
|
|
50
|
+
throw new GenerationOperationError("CONFIG_INVALID", "Project description root does not match requested project root");
|
|
51
|
+
}
|
|
52
|
+
if (input.project.packageName !== config.run.packageName) {
|
|
53
|
+
throw new GenerationOperationError("CONFIG_INVALID", "Project package does not match configured package");
|
|
54
|
+
}
|
|
55
|
+
const validation = await this.dependencies.contextValidator.validate({
|
|
56
|
+
context: input.context,
|
|
57
|
+
projectRoot: input.projectRoot,
|
|
58
|
+
config
|
|
59
|
+
});
|
|
60
|
+
if (validation.status !== "valid") {
|
|
61
|
+
throw new GenerationOperationError(validation.status === "stale" ? "CONTEXT_STALE" : "CONTEXT_INVALID", validation.reason.message);
|
|
62
|
+
}
|
|
63
|
+
const context = ProjectContextSchema.parse(input.context);
|
|
64
|
+
if (context.packageName !== input.project.packageName
|
|
65
|
+
|| context.launchActivity !== input.project.launchActivity) {
|
|
66
|
+
throw new GenerationOperationError("CONTEXT_INVALID", "Context package and launch identity do not match the project");
|
|
67
|
+
}
|
|
68
|
+
const generationId = this.dependencies.generateId();
|
|
69
|
+
const runId = distinctId(generationId, this.dependencies.generateId);
|
|
70
|
+
const session = GenerationSessionSchema.parse({
|
|
71
|
+
version: 1,
|
|
72
|
+
id: generationId,
|
|
73
|
+
revision: 0,
|
|
74
|
+
state: "active",
|
|
75
|
+
bindings: {
|
|
76
|
+
projectHash: hashGenerationBinding(input.project),
|
|
77
|
+
configHash: hashGenerationBinding(config),
|
|
78
|
+
contextHash: hashGenerationBinding(context),
|
|
79
|
+
snapshotHash: null
|
|
80
|
+
},
|
|
81
|
+
target: {
|
|
82
|
+
packageName: config.run.packageName,
|
|
83
|
+
deviceSerial: input.deviceSerial,
|
|
84
|
+
resetStrategy: "processOnly",
|
|
85
|
+
interactionPolicy: context.interactionPolicy
|
|
86
|
+
},
|
|
87
|
+
variables: {
|
|
88
|
+
runId,
|
|
89
|
+
timestamp: this.dependencies.now().toISOString(),
|
|
90
|
+
randomHex: randomHex(this.dependencies.randomBytes(16))
|
|
91
|
+
},
|
|
92
|
+
candidateSteps: [],
|
|
93
|
+
candidateSources: [],
|
|
94
|
+
inFlight: null,
|
|
95
|
+
pendingConfirmation: null,
|
|
96
|
+
verification: { status: "notRun" },
|
|
97
|
+
publication: { status: "notRun" }
|
|
98
|
+
});
|
|
99
|
+
await this.dependencies.store.create(session);
|
|
100
|
+
return session;
|
|
101
|
+
};
|
|
102
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { type PendingConfirmation } from "../../domain/generation.js";
|
|
2
|
+
import { type JourneyStep } from "../../domain/journey.js";
|
|
3
|
+
import { type ProposedStep } from "../../domain/proposed-step.js";
|
|
4
|
+
import { type RuntimeSnapshot } from "../../domain/runtime-snapshot.js";
|
|
5
|
+
import type { AdbPort } from "../../ports/adb.js";
|
|
6
|
+
import type { AndroidCliPort } from "../../ports/android-cli.js";
|
|
7
|
+
import type { Clock } from "../../ports/clock.js";
|
|
8
|
+
import type { GenerationSessionStore } from "../../ports/generation-session-store.js";
|
|
9
|
+
import { type IdleConfig } from "../wait/idle-waiter.js";
|
|
10
|
+
import type { SnapshotReobservationGuard } from "./runtime-observer.js";
|
|
11
|
+
export type GenerationCandidateSource = "planner" | "manualOverride";
|
|
12
|
+
export interface GenerationStepExecutionInput {
|
|
13
|
+
generationId: string;
|
|
14
|
+
proposal: ProposedStep;
|
|
15
|
+
snapshot: RuntimeSnapshot;
|
|
16
|
+
source: GenerationCandidateSource;
|
|
17
|
+
signal?: AbortSignal | undefined;
|
|
18
|
+
}
|
|
19
|
+
export interface GenerationStepFailure {
|
|
20
|
+
code: string;
|
|
21
|
+
message: string;
|
|
22
|
+
}
|
|
23
|
+
export type GenerationStepExecutionResult = {
|
|
24
|
+
status: "succeeded";
|
|
25
|
+
step: JourneyStep;
|
|
26
|
+
} | {
|
|
27
|
+
status: "failed";
|
|
28
|
+
failure: GenerationStepFailure;
|
|
29
|
+
} | {
|
|
30
|
+
status: "cancelled";
|
|
31
|
+
failure: GenerationStepFailure;
|
|
32
|
+
};
|
|
33
|
+
export interface GenerationStepExecutorDependencies {
|
|
34
|
+
store: Pick<GenerationSessionStore, "read" | "beginStep" | "completeStep" | "update" | "writeEvidence" | "writeTextEvidence">;
|
|
35
|
+
freshnessGuard: Pick<SnapshotReobservationGuard, "assertFresh">;
|
|
36
|
+
adb: AdbPort;
|
|
37
|
+
androidCli: AndroidCliPort;
|
|
38
|
+
clock: Clock;
|
|
39
|
+
idle: IdleConfig;
|
|
40
|
+
now: () => Date;
|
|
41
|
+
generateAttemptId: () => string;
|
|
42
|
+
clearApprovedConfirmation: (generationId: string, challenge: PendingConfirmation) => Promise<void>;
|
|
43
|
+
}
|
|
44
|
+
export declare class GenerationStepExecutor {
|
|
45
|
+
private readonly dependencies;
|
|
46
|
+
private readonly riskEvaluator;
|
|
47
|
+
constructor(dependencies: GenerationStepExecutorDependencies);
|
|
48
|
+
readonly execute: (input: GenerationStepExecutionInput) => Promise<GenerationStepExecutionResult>;
|
|
49
|
+
private observeLive;
|
|
50
|
+
private assertForegroundIdentity;
|
|
51
|
+
private markRecovery;
|
|
52
|
+
}
|