agent-scenario-loop 0.1.0
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 +21 -0
- package/README.md +119 -0
- package/app/profile-session.ts +812 -0
- package/core/config-template.json +41 -0
- package/dist/core/agent-summary.d.ts +15 -0
- package/dist/core/agent-summary.js +177 -0
- package/dist/core/artifact-contract.d.ts +151 -0
- package/dist/core/artifact-contract.js +897 -0
- package/dist/core/artifact-layout.d.ts +56 -0
- package/dist/core/artifact-layout.js +61 -0
- package/dist/core/artifact-writer.d.ts +44 -0
- package/dist/core/artifact-writer.js +55 -0
- package/dist/core/comparison.d.ts +133 -0
- package/dist/core/comparison.js +294 -0
- package/dist/core/evidence-interpreter.d.ts +28 -0
- package/dist/core/evidence-interpreter.js +69 -0
- package/dist/core/execution-plan.d.ts +44 -0
- package/dist/core/execution-plan.js +95 -0
- package/dist/core/planner.d.ts +132 -0
- package/dist/core/planner.js +812 -0
- package/dist/core/ports.d.ts +198 -0
- package/dist/core/ports.js +146 -0
- package/dist/core/run-index.d.ts +62 -0
- package/dist/core/run-index.js +143 -0
- package/dist/core/schema-validator.d.ts +86 -0
- package/dist/core/schema-validator.js +407 -0
- package/dist/index.d.ts +11 -0
- package/dist/index.js +27 -0
- package/dist/runner/agent-device-driver.d.ts +126 -0
- package/dist/runner/agent-device-driver.js +168 -0
- package/dist/runner/agent-device.d.ts +295 -0
- package/dist/runner/agent-device.js +1271 -0
- package/dist/runner/android-adb-driver.d.ts +175 -0
- package/dist/runner/android-adb-driver.js +399 -0
- package/dist/runner/android-adb.d.ts +254 -0
- package/dist/runner/android-adb.js +1618 -0
- package/dist/runner/argent-driver.d.ts +183 -0
- package/dist/runner/argent-driver.js +297 -0
- package/dist/runner/argent.d.ts +349 -0
- package/dist/runner/argent.js +1211 -0
- package/dist/runner/check-plan.d.ts +45 -0
- package/dist/runner/check-plan.js +210 -0
- package/dist/runner/cli.d.ts +20 -0
- package/dist/runner/cli.js +23 -0
- package/dist/runner/compare-latest.d.ts +99 -0
- package/dist/runner/compare-latest.js +233 -0
- package/dist/runner/compare.d.ts +58 -0
- package/dist/runner/compare.js +157 -0
- package/dist/runner/demo-loop.d.ts +45 -0
- package/dist/runner/demo-loop.js +170 -0
- package/dist/runner/example-android-live.d.ts +137 -0
- package/dist/runner/example-android-live.js +454 -0
- package/dist/runner/example-ios-live.d.ts +137 -0
- package/dist/runner/example-ios-live.js +471 -0
- package/dist/runner/host-doctor.d.ts +131 -0
- package/dist/runner/host-doctor.js +628 -0
- package/dist/runner/init-project.d.ts +88 -0
- package/dist/runner/init-project.js +263 -0
- package/dist/runner/ios-simctl-driver.d.ts +69 -0
- package/dist/runner/ios-simctl-driver.js +97 -0
- package/dist/runner/ios-simctl.d.ts +254 -0
- package/dist/runner/ios-simctl.js +1415 -0
- package/dist/runner/live-android.d.ts +137 -0
- package/dist/runner/live-android.js +539 -0
- package/dist/runner/live-comparison.d.ts +67 -0
- package/dist/runner/live-comparison.js +147 -0
- package/dist/runner/live-ios.d.ts +137 -0
- package/dist/runner/live-ios.js +460 -0
- package/dist/runner/live-proof-summary.d.ts +263 -0
- package/dist/runner/live-proof-summary.js +465 -0
- package/dist/runner/live-proof.d.ts +467 -0
- package/dist/runner/live-proof.js +920 -0
- package/dist/runner/local-env.d.ts +64 -0
- package/dist/runner/local-env.js +155 -0
- package/dist/runner/profile-android.d.ts +82 -0
- package/dist/runner/profile-android.js +671 -0
- package/dist/runner/profile-ios.d.ts +108 -0
- package/dist/runner/profile-ios.js +532 -0
- package/dist/runner/profile-mobile.d.ts +254 -0
- package/dist/runner/profile-mobile.js +1307 -0
- package/dist/runner/validate-project.d.ts +273 -0
- package/dist/runner/validate-project.js +1501 -0
- package/docs/adapters.md +145 -0
- package/docs/api.md +94 -0
- package/docs/authoring.md +196 -0
- package/docs/concepts.md +136 -0
- package/docs/consumer-rehearsal.md +115 -0
- package/docs/contracts.md +267 -0
- package/docs/live-proofs.md +270 -0
- package/docs/principles.md +46 -0
- package/examples/event-logs/app-startup-baseline.log +4 -0
- package/examples/event-logs/app-startup-current.log +4 -0
- package/examples/minimal-app/README.md +70 -0
- package/examples/mobile-app/README.md +302 -0
- package/examples/mobile-app/app.json +22 -0
- package/examples/mobile-app/asl/package-scripts.json +32 -0
- package/examples/mobile-app/asl.config.json +37 -0
- package/examples/mobile-app/event-logs/android-app-startup.log +4 -0
- package/examples/mobile-app/event-logs/android-open-close-cycle.log +12 -0
- package/examples/mobile-app/event-logs/android-scroll-settle.log +12 -0
- package/examples/mobile-app/event-logs/app-startup.log +4 -0
- package/examples/mobile-app/event-logs/open-close-cycle.log +12 -0
- package/examples/mobile-app/event-logs/scroll-settle.log +12 -0
- package/examples/mobile-app/index.ts +20 -0
- package/examples/mobile-app/metro.config.js +20 -0
- package/examples/mobile-app/package.json +62 -0
- package/examples/mobile-app/patches/expo-modules-jsi@56.0.10.patch +19 -0
- package/examples/mobile-app/plugins/with-ios-build-compat.js +271 -0
- package/examples/mobile-app/pnpm-lock.yaml +4440 -0
- package/examples/mobile-app/runner-manifests/evidence-provider.json +79 -0
- package/examples/mobile-app/runner-manifests/primary-runner.json +19 -0
- package/examples/mobile-app/scenarios/android/app-startup-video.json +73 -0
- package/examples/mobile-app/scenarios/android/app-startup.json +44 -0
- package/examples/mobile-app/scenarios/android/open-close-cycle.json +54 -0
- package/examples/mobile-app/scenarios/android/scroll-settle.json +49 -0
- package/examples/mobile-app/scenarios/ios/app-startup.json +44 -0
- package/examples/mobile-app/scenarios/ios/open-close-cycle.json +54 -0
- package/examples/mobile-app/scenarios/ios/scroll-settle.json +49 -0
- package/examples/mobile-app/scenarios/mobile/app-startup.json +91 -0
- package/examples/mobile-app/scenarios/mobile/open-close-cycle.json +160 -0
- package/examples/mobile-app/scenarios/mobile/scroll-settle.json +148 -0
- package/examples/mobile-app/scripts/asl-capture-accessibility-provider.mjs +112 -0
- package/examples/mobile-app/scripts/asl-capture-profiler-provider.mjs +127 -0
- package/examples/mobile-app/src/devtools/profile-session.ts +7 -0
- package/examples/mobile-app/src/example-screen.tsx +322 -0
- package/examples/mobile-app/tsconfig.json +16 -0
- package/examples/mobile-app/tsconfig.typecheck.json +13 -0
- package/examples/runners/README.md +44 -0
- package/examples/runners/adb-android.json +25 -0
- package/examples/runners/agent-device-android.json +27 -0
- package/examples/runners/agent-device-ios.json +27 -0
- package/examples/runners/argent-android.json +32 -0
- package/examples/runners/argent-ios.json +32 -0
- package/examples/runners/argent-react-profiler-provider.json +15 -0
- package/examples/runners/axe-accessibility-provider.json +24 -0
- package/examples/runners/manual-log-ingest.json +9 -0
- package/examples/runners/rozenite-profiler-provider.json +9 -0
- package/examples/runners/script-accessibility-provider.json +24 -0
- package/examples/runners/script-memory-provider.json +24 -0
- package/examples/runners/script-network-provider.json +24 -0
- package/examples/runners/script-profiler-provider.json +30 -0
- package/examples/runners/xcodebuildmcp-ios.json +29 -0
- package/examples/scenarios/ios/app-startup.json +28 -0
- package/examples/scenarios/ios/open-close-cycle.json +35 -0
- package/examples/scenarios/mobile/app-startup.json +72 -0
- package/examples/scenarios/mobile/media-open-close.json +141 -0
- package/examples/scenarios/mobile/open-close-cycle.json +135 -0
- package/examples/scenarios/mobile/scroll-settle.json +106 -0
- package/package.json +240 -0
- package/schemas/budget-verdict.schema.json +115 -0
- package/schemas/causal-run.schema.json +279 -0
- package/schemas/comparison.schema.json +196 -0
- package/schemas/health.schema.json +108 -0
- package/schemas/live-proof-set.schema.json +195 -0
- package/schemas/live-proof.schema.json +413 -0
- package/schemas/manifest.schema.json +204 -0
- package/schemas/metrics.schema.json +137 -0
- package/schemas/project-validation.schema.json +343 -0
- package/schemas/runner-capabilities.schema.json +217 -0
- package/schemas/scenario.schema.json +400 -0
- package/schemas/verdict.schema.json +88 -0
- package/templates/evidence-provider.json +83 -0
- package/templates/gitignore-snippet +9 -0
- package/templates/integration-readme.md +125 -0
- package/templates/mobile-scenario.json +133 -0
- package/templates/package-scripts.json +32 -0
- package/templates/primary-runner.json +19 -0
- package/templates/project.config.json +37 -0
- package/templates/scripts/asl-capture-accessibility-provider.mjs +112 -0
- package/templates/scripts/asl-capture-profiler-provider.mjs +127 -0
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
declare const PRIMARY_RUNNER_PORT: string[];
|
|
2
|
+
declare const EVIDENCE_PROVIDER_PORT: string[];
|
|
3
|
+
declare const DRIVER_PORT: string[];
|
|
4
|
+
declare const ARTIFACT_WRITER_PORT: string[];
|
|
5
|
+
declare const INTERPRETER_PORT: string[];
|
|
6
|
+
type ScenarioExecutionStep = import('./execution-plan').ScenarioExecutionStep;
|
|
7
|
+
type MaybePromise<T> = T | Promise<T>;
|
|
8
|
+
type PortMetadataValue = string | number | boolean | null | string[] | number[] | boolean[];
|
|
9
|
+
type PortMetadata = Record<string, PortMetadataValue>;
|
|
10
|
+
type PortStatus = 'passed' | 'failed' | 'partial' | 'skipped';
|
|
11
|
+
type PortArtifactMap = Record<string, string | string[] | null>;
|
|
12
|
+
type PortResult = {
|
|
13
|
+
artifacts?: PortArtifactMap;
|
|
14
|
+
message?: string;
|
|
15
|
+
metadata?: PortMetadata;
|
|
16
|
+
status: PortStatus;
|
|
17
|
+
};
|
|
18
|
+
type PortContext = {
|
|
19
|
+
artifactRoot?: string;
|
|
20
|
+
config?: Record<string, unknown>;
|
|
21
|
+
metadata?: PortMetadata;
|
|
22
|
+
platform: string;
|
|
23
|
+
runDir?: string;
|
|
24
|
+
runId: string;
|
|
25
|
+
runner?: Record<string, unknown>;
|
|
26
|
+
scenario?: Record<string, unknown>;
|
|
27
|
+
scenarioId: string;
|
|
28
|
+
};
|
|
29
|
+
type PrimaryRunnerContext = PortContext & {
|
|
30
|
+
executionPlan?: {
|
|
31
|
+
steps: ScenarioExecutionStep[];
|
|
32
|
+
};
|
|
33
|
+
};
|
|
34
|
+
type PrimaryRunnerStepContext = PrimaryRunnerContext & {
|
|
35
|
+
step: ScenarioExecutionStep;
|
|
36
|
+
};
|
|
37
|
+
type EvidenceProviderPhase = 'prepare' | 'startWindow' | 'capture' | 'stopWindow' | 'finalize';
|
|
38
|
+
type EvidenceProviderContext = PortContext & {
|
|
39
|
+
phase?: EvidenceProviderPhase;
|
|
40
|
+
provider?: Record<string, unknown>;
|
|
41
|
+
providerId?: string;
|
|
42
|
+
};
|
|
43
|
+
type DriverActionName = 'tap' | 'scroll' | 'assertVisible' | 'inspectTree' | 'screenshot' | 'record' | 'readLogs' | 'collectPerfSignals';
|
|
44
|
+
type DriverActionInput = {
|
|
45
|
+
action: string;
|
|
46
|
+
artifactPath?: string;
|
|
47
|
+
metadata?: PortMetadata;
|
|
48
|
+
platform: string;
|
|
49
|
+
selector?: Record<string, unknown>;
|
|
50
|
+
step?: ScenarioExecutionStep;
|
|
51
|
+
timeoutMs?: number;
|
|
52
|
+
};
|
|
53
|
+
type DriverActionResult = PortResult & {
|
|
54
|
+
outputPath?: string;
|
|
55
|
+
value?: unknown;
|
|
56
|
+
};
|
|
57
|
+
type ArtifactWriterJsonInput = {
|
|
58
|
+
filePath: string;
|
|
59
|
+
label?: string;
|
|
60
|
+
schema?: unknown;
|
|
61
|
+
value: unknown;
|
|
62
|
+
};
|
|
63
|
+
type ArtifactWriterTextInput = {
|
|
64
|
+
content: string;
|
|
65
|
+
filePath: string;
|
|
66
|
+
};
|
|
67
|
+
type ArtifactWriterCopyInput = {
|
|
68
|
+
destinationPath: string;
|
|
69
|
+
sourcePath: string;
|
|
70
|
+
};
|
|
71
|
+
type InterpreterContext = {
|
|
72
|
+
comparison?: Record<string, unknown>;
|
|
73
|
+
evidence: Record<string, unknown>;
|
|
74
|
+
metadata?: PortMetadata;
|
|
75
|
+
profile?: Record<string, unknown>;
|
|
76
|
+
scenarioHealthPassed: boolean;
|
|
77
|
+
};
|
|
78
|
+
type InterpreterResult = {
|
|
79
|
+
likelyCauses: string[];
|
|
80
|
+
metadata?: PortMetadata;
|
|
81
|
+
notes: string[];
|
|
82
|
+
summary: string;
|
|
83
|
+
trusted: boolean;
|
|
84
|
+
};
|
|
85
|
+
type PrimaryRunnerPort = {
|
|
86
|
+
captureEvidence(context: PrimaryRunnerStepContext): MaybePromise<PortResult>;
|
|
87
|
+
executeStep(context: PrimaryRunnerStepContext): MaybePromise<PortResult>;
|
|
88
|
+
finalize(context: PrimaryRunnerContext): MaybePromise<PortResult>;
|
|
89
|
+
launch(context: PrimaryRunnerContext): MaybePromise<PortResult>;
|
|
90
|
+
prepare(context: PrimaryRunnerContext): MaybePromise<PortResult>;
|
|
91
|
+
startSession(context: PrimaryRunnerContext): MaybePromise<PortResult>;
|
|
92
|
+
stopSession(context: PrimaryRunnerContext): MaybePromise<PortResult>;
|
|
93
|
+
waitForTruthEvent(context: PrimaryRunnerStepContext): MaybePromise<PortResult>;
|
|
94
|
+
};
|
|
95
|
+
type EvidenceProviderPort = {
|
|
96
|
+
capture(context: EvidenceProviderContext): MaybePromise<PortResult>;
|
|
97
|
+
finalize(context: EvidenceProviderContext): MaybePromise<PortResult>;
|
|
98
|
+
prepare(context: EvidenceProviderContext): MaybePromise<PortResult>;
|
|
99
|
+
startWindow(context: EvidenceProviderContext): MaybePromise<PortResult>;
|
|
100
|
+
stopWindow(context: EvidenceProviderContext): MaybePromise<PortResult>;
|
|
101
|
+
};
|
|
102
|
+
type DriverPort = {
|
|
103
|
+
assertVisible(input: DriverActionInput): MaybePromise<DriverActionResult>;
|
|
104
|
+
collectPerfSignals(input: DriverActionInput): MaybePromise<DriverActionResult>;
|
|
105
|
+
inspectTree(input: DriverActionInput): MaybePromise<DriverActionResult>;
|
|
106
|
+
readLogs(input: DriverActionInput): MaybePromise<DriverActionResult>;
|
|
107
|
+
record(input: DriverActionInput): MaybePromise<DriverActionResult>;
|
|
108
|
+
screenshot(input: DriverActionInput): MaybePromise<DriverActionResult>;
|
|
109
|
+
scroll(input: DriverActionInput): MaybePromise<DriverActionResult>;
|
|
110
|
+
tap(input: DriverActionInput): MaybePromise<DriverActionResult>;
|
|
111
|
+
};
|
|
112
|
+
type ArtifactWriterPort = {
|
|
113
|
+
copyRaw(input: ArtifactWriterCopyInput): MaybePromise<string>;
|
|
114
|
+
writeJson(input: ArtifactWriterJsonInput): MaybePromise<void>;
|
|
115
|
+
writeText(input: ArtifactWriterTextInput): MaybePromise<void>;
|
|
116
|
+
};
|
|
117
|
+
type InterpreterPort = {
|
|
118
|
+
interpret(context: InterpreterContext): MaybePromise<InterpreterResult>;
|
|
119
|
+
};
|
|
120
|
+
/**
|
|
121
|
+
* Returns true when a scenario action maps to the stable driver port surface.
|
|
122
|
+
*
|
|
123
|
+
* @param {string} action
|
|
124
|
+
* @returns {boolean}
|
|
125
|
+
*/
|
|
126
|
+
declare function isDriverActionName(action: string): action is DriverActionName;
|
|
127
|
+
/**
|
|
128
|
+
* Dispatches one normalized scenario driver action to a swappable driver.
|
|
129
|
+
*
|
|
130
|
+
* Runners should call this after planner compatibility has passed. The runtime
|
|
131
|
+
* guard still fails explicitly so missing adapter capabilities do not degrade
|
|
132
|
+
* into silent no-ops.
|
|
133
|
+
*
|
|
134
|
+
* @param {{driver: Partial<DriverPort> & Record<string, unknown>, input: DriverActionInput}} options
|
|
135
|
+
* @returns {Promise<DriverActionResult>}
|
|
136
|
+
*/
|
|
137
|
+
declare function dispatchDriverAction({ driver, input, }: {
|
|
138
|
+
driver: Partial<DriverPort> & PortImplementation;
|
|
139
|
+
input: DriverActionInput;
|
|
140
|
+
}): Promise<DriverActionResult>;
|
|
141
|
+
/**
|
|
142
|
+
* Returns method names missing from an implementation object.
|
|
143
|
+
*
|
|
144
|
+
* @param {Record<string, unknown>} implementation
|
|
145
|
+
* @param {string[]} requiredMethods
|
|
146
|
+
* @returns {string[]}
|
|
147
|
+
*/
|
|
148
|
+
declare function missingPortMethods(implementation: PortImplementation, requiredMethods: string[]): string[];
|
|
149
|
+
/**
|
|
150
|
+
* Returns callable method names exposed by an implementation object.
|
|
151
|
+
*
|
|
152
|
+
* @param {Record<string, unknown>} implementation
|
|
153
|
+
* @returns {string[]}
|
|
154
|
+
*/
|
|
155
|
+
declare function implementedPortMethods(implementation: PortImplementation): string[];
|
|
156
|
+
/**
|
|
157
|
+
* Builds a stable human-readable port validation message.
|
|
158
|
+
*
|
|
159
|
+
* @param {{name: string, missingMethods: string[]}} options
|
|
160
|
+
* @returns {string}
|
|
161
|
+
*/
|
|
162
|
+
declare function buildPortValidationMessage({ name, missingMethods, }: {
|
|
163
|
+
name: string;
|
|
164
|
+
missingMethods: string[];
|
|
165
|
+
}): string;
|
|
166
|
+
/**
|
|
167
|
+
* Validates whether an implementation satisfies a named port.
|
|
168
|
+
*
|
|
169
|
+
* @param {{name: string, implementation: Record<string, unknown>, requiredMethods: string[]}} options
|
|
170
|
+
* @returns {{valid: boolean, name: string, expectedMethods: string[], implementedMethods: string[], missingMethods: string[], message: string}}
|
|
171
|
+
*/
|
|
172
|
+
declare function validatePortImplementation({ name, implementation, requiredMethods, }: {
|
|
173
|
+
name: string;
|
|
174
|
+
implementation: PortImplementation;
|
|
175
|
+
requiredMethods: string[];
|
|
176
|
+
}): PortValidationResult;
|
|
177
|
+
/**
|
|
178
|
+
* Asserts that an implementation satisfies a named port.
|
|
179
|
+
*
|
|
180
|
+
* @param {{name: string, implementation: Record<string, unknown>, requiredMethods: string[]}} options
|
|
181
|
+
* @returns {Record<string, unknown>}
|
|
182
|
+
*/
|
|
183
|
+
declare function assertPortImplementation({ name, implementation, requiredMethods, }: {
|
|
184
|
+
name: string;
|
|
185
|
+
implementation: PortImplementation;
|
|
186
|
+
requiredMethods: string[];
|
|
187
|
+
}): PortImplementation;
|
|
188
|
+
export { ARTIFACT_WRITER_PORT, DRIVER_PORT, EVIDENCE_PROVIDER_PORT, INTERPRETER_PORT, PRIMARY_RUNNER_PORT, assertPortImplementation, buildPortValidationMessage, dispatchDriverAction, implementedPortMethods, isDriverActionName, missingPortMethods, validatePortImplementation, };
|
|
189
|
+
export type { ArtifactWriterCopyInput, ArtifactWriterJsonInput, ArtifactWriterPort, ArtifactWriterTextInput, DriverActionName, DriverActionInput, DriverActionResult, DriverPort, EvidenceProviderContext, EvidenceProviderPhase, EvidenceProviderPort, InterpreterContext, InterpreterPort, InterpreterResult, MaybePromise, PortArtifactMap, PortContext, PortImplementation, PortMetadata, PortMetadataValue, PortResult, PortStatus, PrimaryRunnerContext, PrimaryRunnerPort, PrimaryRunnerStepContext, PortValidationResult, };
|
|
190
|
+
type PortImplementation = Record<string, unknown>;
|
|
191
|
+
type PortValidationResult = {
|
|
192
|
+
expectedMethods: string[];
|
|
193
|
+
implementedMethods: string[];
|
|
194
|
+
message: string;
|
|
195
|
+
missingMethods: string[];
|
|
196
|
+
valid: boolean;
|
|
197
|
+
name: string;
|
|
198
|
+
};
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.PRIMARY_RUNNER_PORT = exports.INTERPRETER_PORT = exports.EVIDENCE_PROVIDER_PORT = exports.DRIVER_PORT = exports.ARTIFACT_WRITER_PORT = void 0;
|
|
4
|
+
exports.assertPortImplementation = assertPortImplementation;
|
|
5
|
+
exports.buildPortValidationMessage = buildPortValidationMessage;
|
|
6
|
+
exports.dispatchDriverAction = dispatchDriverAction;
|
|
7
|
+
exports.implementedPortMethods = implementedPortMethods;
|
|
8
|
+
exports.isDriverActionName = isDriverActionName;
|
|
9
|
+
exports.missingPortMethods = missingPortMethods;
|
|
10
|
+
exports.validatePortImplementation = validatePortImplementation;
|
|
11
|
+
const PRIMARY_RUNNER_PORT = [
|
|
12
|
+
'prepare',
|
|
13
|
+
'launch',
|
|
14
|
+
'startSession',
|
|
15
|
+
'executeStep',
|
|
16
|
+
'waitForTruthEvent',
|
|
17
|
+
'captureEvidence',
|
|
18
|
+
'stopSession',
|
|
19
|
+
'finalize',
|
|
20
|
+
];
|
|
21
|
+
exports.PRIMARY_RUNNER_PORT = PRIMARY_RUNNER_PORT;
|
|
22
|
+
const EVIDENCE_PROVIDER_PORT = [
|
|
23
|
+
'prepare',
|
|
24
|
+
'startWindow',
|
|
25
|
+
'capture',
|
|
26
|
+
'stopWindow',
|
|
27
|
+
'finalize',
|
|
28
|
+
];
|
|
29
|
+
exports.EVIDENCE_PROVIDER_PORT = EVIDENCE_PROVIDER_PORT;
|
|
30
|
+
const DRIVER_PORT = [
|
|
31
|
+
'tap',
|
|
32
|
+
'scroll',
|
|
33
|
+
'assertVisible',
|
|
34
|
+
'inspectTree',
|
|
35
|
+
'screenshot',
|
|
36
|
+
'record',
|
|
37
|
+
'readLogs',
|
|
38
|
+
'collectPerfSignals',
|
|
39
|
+
];
|
|
40
|
+
exports.DRIVER_PORT = DRIVER_PORT;
|
|
41
|
+
const ARTIFACT_WRITER_PORT = [
|
|
42
|
+
'writeJson',
|
|
43
|
+
'writeText',
|
|
44
|
+
'copyRaw',
|
|
45
|
+
];
|
|
46
|
+
exports.ARTIFACT_WRITER_PORT = ARTIFACT_WRITER_PORT;
|
|
47
|
+
const INTERPRETER_PORT = [
|
|
48
|
+
'interpret',
|
|
49
|
+
];
|
|
50
|
+
exports.INTERPRETER_PORT = INTERPRETER_PORT;
|
|
51
|
+
/**
|
|
52
|
+
* Returns true when a scenario action maps to the stable driver port surface.
|
|
53
|
+
*
|
|
54
|
+
* @param {string} action
|
|
55
|
+
* @returns {boolean}
|
|
56
|
+
*/
|
|
57
|
+
function isDriverActionName(action) {
|
|
58
|
+
return DRIVER_PORT.includes(action);
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Dispatches one normalized scenario driver action to a swappable driver.
|
|
62
|
+
*
|
|
63
|
+
* Runners should call this after planner compatibility has passed. The runtime
|
|
64
|
+
* guard still fails explicitly so missing adapter capabilities do not degrade
|
|
65
|
+
* into silent no-ops.
|
|
66
|
+
*
|
|
67
|
+
* @param {{driver: Partial<DriverPort> & Record<string, unknown>, input: DriverActionInput}} options
|
|
68
|
+
* @returns {Promise<DriverActionResult>}
|
|
69
|
+
*/
|
|
70
|
+
async function dispatchDriverAction({ driver, input, }) {
|
|
71
|
+
if (!isDriverActionName(input.action)) {
|
|
72
|
+
throw new Error(`Unsupported driver action \`${input.action}\`.`);
|
|
73
|
+
}
|
|
74
|
+
const method = driver[input.action];
|
|
75
|
+
if (typeof method !== 'function') {
|
|
76
|
+
throw new Error(`Driver is missing action \`${input.action}\`.`);
|
|
77
|
+
}
|
|
78
|
+
return method(input);
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Returns method names missing from an implementation object.
|
|
82
|
+
*
|
|
83
|
+
* @param {Record<string, unknown>} implementation
|
|
84
|
+
* @param {string[]} requiredMethods
|
|
85
|
+
* @returns {string[]}
|
|
86
|
+
*/
|
|
87
|
+
function missingPortMethods(implementation, requiredMethods) {
|
|
88
|
+
return requiredMethods.filter((methodName) => typeof implementation?.[methodName] !== 'function');
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Returns callable method names exposed by an implementation object.
|
|
92
|
+
*
|
|
93
|
+
* @param {Record<string, unknown>} implementation
|
|
94
|
+
* @returns {string[]}
|
|
95
|
+
*/
|
|
96
|
+
function implementedPortMethods(implementation) {
|
|
97
|
+
if (!implementation || typeof implementation !== 'object') {
|
|
98
|
+
return [];
|
|
99
|
+
}
|
|
100
|
+
return Object.keys(implementation)
|
|
101
|
+
.filter((methodName) => typeof implementation[methodName] === 'function')
|
|
102
|
+
.sort();
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* Builds a stable human-readable port validation message.
|
|
106
|
+
*
|
|
107
|
+
* @param {{name: string, missingMethods: string[]}} options
|
|
108
|
+
* @returns {string}
|
|
109
|
+
*/
|
|
110
|
+
function buildPortValidationMessage({ name, missingMethods, }) {
|
|
111
|
+
if (missingMethods.length === 0) {
|
|
112
|
+
return `${name} satisfies the required port methods.`;
|
|
113
|
+
}
|
|
114
|
+
return `${name} is missing required method(s): ${missingMethods.join(', ')}`;
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* Validates whether an implementation satisfies a named port.
|
|
118
|
+
*
|
|
119
|
+
* @param {{name: string, implementation: Record<string, unknown>, requiredMethods: string[]}} options
|
|
120
|
+
* @returns {{valid: boolean, name: string, expectedMethods: string[], implementedMethods: string[], missingMethods: string[], message: string}}
|
|
121
|
+
*/
|
|
122
|
+
function validatePortImplementation({ name, implementation, requiredMethods, }) {
|
|
123
|
+
const missingMethods = missingPortMethods(implementation, requiredMethods);
|
|
124
|
+
const implementedMethods = implementedPortMethods(implementation);
|
|
125
|
+
return {
|
|
126
|
+
valid: missingMethods.length === 0,
|
|
127
|
+
name,
|
|
128
|
+
expectedMethods: [...requiredMethods],
|
|
129
|
+
implementedMethods,
|
|
130
|
+
missingMethods,
|
|
131
|
+
message: buildPortValidationMessage({ name, missingMethods }),
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* Asserts that an implementation satisfies a named port.
|
|
136
|
+
*
|
|
137
|
+
* @param {{name: string, implementation: Record<string, unknown>, requiredMethods: string[]}} options
|
|
138
|
+
* @returns {Record<string, unknown>}
|
|
139
|
+
*/
|
|
140
|
+
function assertPortImplementation({ name, implementation, requiredMethods, }) {
|
|
141
|
+
const result = validatePortImplementation({ name, implementation, requiredMethods });
|
|
142
|
+
if (!result.valid) {
|
|
143
|
+
throw new Error(result.message);
|
|
144
|
+
}
|
|
145
|
+
return implementation;
|
|
146
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
type RunIndexEntry = {
|
|
2
|
+
runDir: string;
|
|
3
|
+
scenarioId: string;
|
|
4
|
+
scenarioHash?: string;
|
|
5
|
+
runId: string;
|
|
6
|
+
healthStatus: string;
|
|
7
|
+
trusted: boolean;
|
|
8
|
+
durationMs?: number;
|
|
9
|
+
endedAt?: string;
|
|
10
|
+
flowId?: string;
|
|
11
|
+
comparisonLane?: string;
|
|
12
|
+
interactionDriver?: string;
|
|
13
|
+
platform?: string;
|
|
14
|
+
startedAt?: string;
|
|
15
|
+
verdictStatus?: string;
|
|
16
|
+
};
|
|
17
|
+
type RunIndex = {
|
|
18
|
+
entries: RunIndexEntry[];
|
|
19
|
+
rootDir: string;
|
|
20
|
+
trusted: RunIndexEntry[];
|
|
21
|
+
};
|
|
22
|
+
/**
|
|
23
|
+
* Recursively finds run directories under one artifact root.
|
|
24
|
+
*
|
|
25
|
+
* @param {string} rootDir
|
|
26
|
+
* @returns {string[]}
|
|
27
|
+
*/
|
|
28
|
+
declare function findRunDirs(rootDir: string): string[];
|
|
29
|
+
/**
|
|
30
|
+
* Converts one run directory into an index entry.
|
|
31
|
+
*
|
|
32
|
+
* @param {string} runDir
|
|
33
|
+
* @returns {RunIndexEntry}
|
|
34
|
+
*/
|
|
35
|
+
declare function readRunIndexEntry(runDir: string): RunIndexEntry;
|
|
36
|
+
/**
|
|
37
|
+
* Sorts entries newest first while keeping deterministic fallback ordering.
|
|
38
|
+
*
|
|
39
|
+
* @param {RunIndexEntry[]} entries
|
|
40
|
+
* @returns {RunIndexEntry[]}
|
|
41
|
+
*/
|
|
42
|
+
declare function sortRunIndexEntries(entries: RunIndexEntry[]): RunIndexEntry[];
|
|
43
|
+
/**
|
|
44
|
+
* Builds a read-only index of completed run artifact folders.
|
|
45
|
+
*
|
|
46
|
+
* @param {{rootDir: string, scenarioId?: string}} options
|
|
47
|
+
* @returns {RunIndex}
|
|
48
|
+
*/
|
|
49
|
+
declare function buildRunIndex({ rootDir, scenarioId }: {
|
|
50
|
+
rootDir: string;
|
|
51
|
+
scenarioId?: string;
|
|
52
|
+
}): RunIndex;
|
|
53
|
+
/**
|
|
54
|
+
* Returns the newest trusted run for a scenario, if one exists.
|
|
55
|
+
*
|
|
56
|
+
* @param {RunIndex} index
|
|
57
|
+
* @param {string} scenarioId
|
|
58
|
+
* @returns {RunIndexEntry | null}
|
|
59
|
+
*/
|
|
60
|
+
declare function findLatestTrustedRun(index: RunIndex, scenarioId: string): RunIndexEntry | null;
|
|
61
|
+
export { buildRunIndex, findLatestTrustedRun, findRunDirs, readRunIndexEntry, sortRunIndexEntries, };
|
|
62
|
+
export type { RunIndex, RunIndexEntry, };
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.buildRunIndex = buildRunIndex;
|
|
4
|
+
exports.findLatestTrustedRun = findLatestTrustedRun;
|
|
5
|
+
exports.findRunDirs = findRunDirs;
|
|
6
|
+
exports.readRunIndexEntry = readRunIndexEntry;
|
|
7
|
+
exports.sortRunIndexEntries = sortRunIndexEntries;
|
|
8
|
+
const fs = require('node:fs');
|
|
9
|
+
const path = require('node:path');
|
|
10
|
+
const { ARTIFACT_FILENAMES, PROFILE_ARTIFACT_FILENAMES } = require('./artifact-layout');
|
|
11
|
+
/**
|
|
12
|
+
* Reads a JSON object from disk.
|
|
13
|
+
*
|
|
14
|
+
* @param {string} filePath
|
|
15
|
+
* @returns {Record<string, unknown>}
|
|
16
|
+
*/
|
|
17
|
+
function readJson(filePath) {
|
|
18
|
+
return JSON.parse(fs.readFileSync(filePath, 'utf8'));
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Returns whether a directory contains the minimum run artifact pair.
|
|
22
|
+
*
|
|
23
|
+
* @param {string} dir
|
|
24
|
+
* @returns {boolean}
|
|
25
|
+
*/
|
|
26
|
+
function isRunDir(dir) {
|
|
27
|
+
return (fs.existsSync(path.join(dir, ARTIFACT_FILENAMES.health)) &&
|
|
28
|
+
fs.existsSync(path.join(dir, ARTIFACT_FILENAMES.verdict)));
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Recursively finds run directories under one artifact root.
|
|
32
|
+
*
|
|
33
|
+
* @param {string} rootDir
|
|
34
|
+
* @returns {string[]}
|
|
35
|
+
*/
|
|
36
|
+
function findRunDirs(rootDir) {
|
|
37
|
+
if (!fs.existsSync(rootDir) || !fs.statSync(rootDir).isDirectory()) {
|
|
38
|
+
return [];
|
|
39
|
+
}
|
|
40
|
+
const results = [];
|
|
41
|
+
const pending = [path.resolve(rootDir)];
|
|
42
|
+
while (pending.length > 0) {
|
|
43
|
+
const current = pending.pop();
|
|
44
|
+
if (!current) {
|
|
45
|
+
continue;
|
|
46
|
+
}
|
|
47
|
+
if (isRunDir(current)) {
|
|
48
|
+
results.push(current);
|
|
49
|
+
continue;
|
|
50
|
+
}
|
|
51
|
+
for (const entry of fs.readdirSync(current, { withFileTypes: true })) {
|
|
52
|
+
if (entry.isDirectory()) {
|
|
53
|
+
pending.push(path.join(current, entry.name));
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
return results.sort();
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Converts one run directory into an index entry.
|
|
61
|
+
*
|
|
62
|
+
* @param {string} runDir
|
|
63
|
+
* @returns {RunIndexEntry}
|
|
64
|
+
*/
|
|
65
|
+
function readRunIndexEntry(runDir) {
|
|
66
|
+
const health = readJson(path.join(runDir, ARTIFACT_FILENAMES.health));
|
|
67
|
+
const verdict = readJson(path.join(runDir, ARTIFACT_FILENAMES.verdict));
|
|
68
|
+
const manifestPath = path.join(runDir, PROFILE_ARTIFACT_FILENAMES.manifest);
|
|
69
|
+
const manifest = fs.existsSync(manifestPath) ? readJson(manifestPath) : {};
|
|
70
|
+
const scenarioId = typeof health.scenarioId === 'string'
|
|
71
|
+
? health.scenarioId
|
|
72
|
+
: typeof manifest.scenario === 'string'
|
|
73
|
+
? manifest.scenario
|
|
74
|
+
: 'unknown-scenario';
|
|
75
|
+
const runId = typeof health.runId === 'string'
|
|
76
|
+
? health.runId
|
|
77
|
+
: typeof manifest.runId === 'string'
|
|
78
|
+
? manifest.runId
|
|
79
|
+
: path.basename(runDir);
|
|
80
|
+
const healthStatus = typeof health.healthStatus === 'string' ? health.healthStatus : 'unknown';
|
|
81
|
+
const verdictStatus = typeof verdict.verdictStatus === 'string' ? verdict.verdictStatus : undefined;
|
|
82
|
+
return {
|
|
83
|
+
runDir,
|
|
84
|
+
scenarioId,
|
|
85
|
+
runId,
|
|
86
|
+
...(typeof manifest.scenarioHash === 'string' ? { scenarioHash: manifest.scenarioHash } : {}),
|
|
87
|
+
healthStatus,
|
|
88
|
+
trusted: healthStatus === 'passed' && verdictStatus === 'passed',
|
|
89
|
+
...(typeof manifest.durationMs === 'number' ? { durationMs: manifest.durationMs } : {}),
|
|
90
|
+
...(typeof manifest.endedAt === 'string' ? { endedAt: manifest.endedAt } : {}),
|
|
91
|
+
...(typeof health.flowId === 'string' ? { flowId: health.flowId } : {}),
|
|
92
|
+
...(typeof manifest.comparisonLane === 'string' ? { comparisonLane: manifest.comparisonLane } : {}),
|
|
93
|
+
...(typeof manifest.interactionDriver === 'string' ? { interactionDriver: manifest.interactionDriver } : {}),
|
|
94
|
+
...(typeof manifest.platform === 'string' ? { platform: manifest.platform } : {}),
|
|
95
|
+
...(typeof manifest.startedAt === 'string' ? { startedAt: manifest.startedAt } : {}),
|
|
96
|
+
...(verdictStatus ? { verdictStatus } : {}),
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Sorts entries newest first while keeping deterministic fallback ordering.
|
|
101
|
+
*
|
|
102
|
+
* @param {RunIndexEntry[]} entries
|
|
103
|
+
* @returns {RunIndexEntry[]}
|
|
104
|
+
*/
|
|
105
|
+
function sortRunIndexEntries(entries) {
|
|
106
|
+
return [...entries].sort((left, right) => {
|
|
107
|
+
const leftTime = Date.parse(left.endedAt ?? left.startedAt ?? '');
|
|
108
|
+
const rightTime = Date.parse(right.endedAt ?? right.startedAt ?? '');
|
|
109
|
+
const leftTimestamp = Number.isFinite(leftTime) ? leftTime : 0;
|
|
110
|
+
const rightTimestamp = Number.isFinite(rightTime) ? rightTime : 0;
|
|
111
|
+
if (leftTimestamp !== rightTimestamp) {
|
|
112
|
+
return rightTimestamp - leftTimestamp;
|
|
113
|
+
}
|
|
114
|
+
return `${left.scenarioId}/${left.runId}/${left.runDir}`.localeCompare(`${right.scenarioId}/${right.runId}/${right.runDir}`);
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* Builds a read-only index of completed run artifact folders.
|
|
119
|
+
*
|
|
120
|
+
* @param {{rootDir: string, scenarioId?: string}} options
|
|
121
|
+
* @returns {RunIndex}
|
|
122
|
+
*/
|
|
123
|
+
function buildRunIndex({ rootDir, scenarioId }) {
|
|
124
|
+
const resolvedRoot = path.resolve(rootDir);
|
|
125
|
+
const entries = sortRunIndexEntries(findRunDirs(resolvedRoot)
|
|
126
|
+
.map((runDir) => readRunIndexEntry(runDir))
|
|
127
|
+
.filter((entry) => !scenarioId || entry.scenarioId === scenarioId));
|
|
128
|
+
return {
|
|
129
|
+
rootDir: resolvedRoot,
|
|
130
|
+
entries,
|
|
131
|
+
trusted: entries.filter((entry) => entry.trusted),
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* Returns the newest trusted run for a scenario, if one exists.
|
|
136
|
+
*
|
|
137
|
+
* @param {RunIndex} index
|
|
138
|
+
* @param {string} scenarioId
|
|
139
|
+
* @returns {RunIndexEntry | null}
|
|
140
|
+
*/
|
|
141
|
+
function findLatestTrustedRun(index, scenarioId) {
|
|
142
|
+
return index.trusted.find((entry) => entry.scenarioId === scenarioId) ?? null;
|
|
143
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
type JsonSchema = Record<string, unknown> & {
|
|
2
|
+
$ref?: string;
|
|
3
|
+
allOf?: JsonSchema[];
|
|
4
|
+
const?: unknown;
|
|
5
|
+
description?: string;
|
|
6
|
+
else?: JsonSchema;
|
|
7
|
+
if?: JsonSchema;
|
|
8
|
+
not?: JsonSchema;
|
|
9
|
+
then?: JsonSchema;
|
|
10
|
+
type?: string | string[];
|
|
11
|
+
enum?: unknown[];
|
|
12
|
+
pattern?: string;
|
|
13
|
+
minLength?: number;
|
|
14
|
+
minimum?: number;
|
|
15
|
+
minItems?: number;
|
|
16
|
+
uniqueItems?: boolean;
|
|
17
|
+
items?: JsonSchema;
|
|
18
|
+
minProperties?: number;
|
|
19
|
+
required?: string[];
|
|
20
|
+
properties?: Record<string, JsonSchema>;
|
|
21
|
+
additionalProperties?: boolean | JsonSchema;
|
|
22
|
+
};
|
|
23
|
+
type ValidationError = {
|
|
24
|
+
code: string;
|
|
25
|
+
path: string;
|
|
26
|
+
message: string;
|
|
27
|
+
};
|
|
28
|
+
type ValidationResult = {
|
|
29
|
+
valid: boolean;
|
|
30
|
+
errors: ValidationError[];
|
|
31
|
+
message: string;
|
|
32
|
+
};
|
|
33
|
+
/**
|
|
34
|
+
* Error thrown when a manifest or generated artifact does not satisfy its schema.
|
|
35
|
+
*/
|
|
36
|
+
declare class SchemaValidationError extends Error {
|
|
37
|
+
label: string;
|
|
38
|
+
errors: ValidationError[];
|
|
39
|
+
/**
|
|
40
|
+
* @param {string} label
|
|
41
|
+
* @param {{code: string, path: string, message: string}[]} errors
|
|
42
|
+
*/
|
|
43
|
+
constructor(label: string, errors: ValidationError[]);
|
|
44
|
+
}
|
|
45
|
+
declare const SCHEMAS: {
|
|
46
|
+
budgetVerdict: JsonSchema;
|
|
47
|
+
causalRun: JsonSchema;
|
|
48
|
+
comparison: JsonSchema;
|
|
49
|
+
health: JsonSchema;
|
|
50
|
+
liveProof: JsonSchema;
|
|
51
|
+
liveProofSet: JsonSchema;
|
|
52
|
+
manifest: JsonSchema;
|
|
53
|
+
metrics: JsonSchema;
|
|
54
|
+
projectValidation: JsonSchema;
|
|
55
|
+
scenario: JsonSchema;
|
|
56
|
+
runnerCapabilities: JsonSchema;
|
|
57
|
+
verdict: JsonSchema;
|
|
58
|
+
};
|
|
59
|
+
/**
|
|
60
|
+
* Validates a value against a schema and returns structured errors instead of throwing.
|
|
61
|
+
*
|
|
62
|
+
* @param {unknown} value
|
|
63
|
+
* @param {Record<string, unknown>} schema
|
|
64
|
+
* @param {string} [label]
|
|
65
|
+
* @returns {{valid: boolean, errors: {code: string, path: string, message: string}[], message: string}}
|
|
66
|
+
*/
|
|
67
|
+
declare function validateJson(value: unknown, schema: JsonSchema, label?: string): ValidationResult;
|
|
68
|
+
/**
|
|
69
|
+
* Validates a value against a schema, throwing `SchemaValidationError` on failure.
|
|
70
|
+
*
|
|
71
|
+
* @param {unknown} value
|
|
72
|
+
* @param {Record<string, unknown>} schema
|
|
73
|
+
* @param {string} label
|
|
74
|
+
* @returns {unknown}
|
|
75
|
+
*/
|
|
76
|
+
declare function assertValidJson(value: unknown, schema: JsonSchema, label: string): unknown;
|
|
77
|
+
/**
|
|
78
|
+
* Formats schema errors as a CLI-readable message.
|
|
79
|
+
*
|
|
80
|
+
* @param {string} label
|
|
81
|
+
* @param {{path: string, message: string}[]} errors
|
|
82
|
+
* @returns {string}
|
|
83
|
+
*/
|
|
84
|
+
declare function formatValidationErrorMessage(label: string, errors: Array<Pick<ValidationError, 'path' | 'message'>>): string;
|
|
85
|
+
export { SCHEMAS, SchemaValidationError, assertValidJson, formatValidationErrorMessage, validateJson, };
|
|
86
|
+
export type { JsonSchema, ValidationError, ValidationResult, };
|