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.
Files changed (170) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +119 -0
  3. package/app/profile-session.ts +812 -0
  4. package/core/config-template.json +41 -0
  5. package/dist/core/agent-summary.d.ts +15 -0
  6. package/dist/core/agent-summary.js +177 -0
  7. package/dist/core/artifact-contract.d.ts +151 -0
  8. package/dist/core/artifact-contract.js +897 -0
  9. package/dist/core/artifact-layout.d.ts +56 -0
  10. package/dist/core/artifact-layout.js +61 -0
  11. package/dist/core/artifact-writer.d.ts +44 -0
  12. package/dist/core/artifact-writer.js +55 -0
  13. package/dist/core/comparison.d.ts +133 -0
  14. package/dist/core/comparison.js +294 -0
  15. package/dist/core/evidence-interpreter.d.ts +28 -0
  16. package/dist/core/evidence-interpreter.js +69 -0
  17. package/dist/core/execution-plan.d.ts +44 -0
  18. package/dist/core/execution-plan.js +95 -0
  19. package/dist/core/planner.d.ts +132 -0
  20. package/dist/core/planner.js +812 -0
  21. package/dist/core/ports.d.ts +198 -0
  22. package/dist/core/ports.js +146 -0
  23. package/dist/core/run-index.d.ts +62 -0
  24. package/dist/core/run-index.js +143 -0
  25. package/dist/core/schema-validator.d.ts +86 -0
  26. package/dist/core/schema-validator.js +407 -0
  27. package/dist/index.d.ts +11 -0
  28. package/dist/index.js +27 -0
  29. package/dist/runner/agent-device-driver.d.ts +126 -0
  30. package/dist/runner/agent-device-driver.js +168 -0
  31. package/dist/runner/agent-device.d.ts +295 -0
  32. package/dist/runner/agent-device.js +1271 -0
  33. package/dist/runner/android-adb-driver.d.ts +175 -0
  34. package/dist/runner/android-adb-driver.js +399 -0
  35. package/dist/runner/android-adb.d.ts +254 -0
  36. package/dist/runner/android-adb.js +1618 -0
  37. package/dist/runner/argent-driver.d.ts +183 -0
  38. package/dist/runner/argent-driver.js +297 -0
  39. package/dist/runner/argent.d.ts +349 -0
  40. package/dist/runner/argent.js +1211 -0
  41. package/dist/runner/check-plan.d.ts +45 -0
  42. package/dist/runner/check-plan.js +210 -0
  43. package/dist/runner/cli.d.ts +20 -0
  44. package/dist/runner/cli.js +23 -0
  45. package/dist/runner/compare-latest.d.ts +99 -0
  46. package/dist/runner/compare-latest.js +233 -0
  47. package/dist/runner/compare.d.ts +58 -0
  48. package/dist/runner/compare.js +157 -0
  49. package/dist/runner/demo-loop.d.ts +45 -0
  50. package/dist/runner/demo-loop.js +170 -0
  51. package/dist/runner/example-android-live.d.ts +137 -0
  52. package/dist/runner/example-android-live.js +454 -0
  53. package/dist/runner/example-ios-live.d.ts +137 -0
  54. package/dist/runner/example-ios-live.js +471 -0
  55. package/dist/runner/host-doctor.d.ts +131 -0
  56. package/dist/runner/host-doctor.js +628 -0
  57. package/dist/runner/init-project.d.ts +88 -0
  58. package/dist/runner/init-project.js +263 -0
  59. package/dist/runner/ios-simctl-driver.d.ts +69 -0
  60. package/dist/runner/ios-simctl-driver.js +97 -0
  61. package/dist/runner/ios-simctl.d.ts +254 -0
  62. package/dist/runner/ios-simctl.js +1415 -0
  63. package/dist/runner/live-android.d.ts +137 -0
  64. package/dist/runner/live-android.js +539 -0
  65. package/dist/runner/live-comparison.d.ts +67 -0
  66. package/dist/runner/live-comparison.js +147 -0
  67. package/dist/runner/live-ios.d.ts +137 -0
  68. package/dist/runner/live-ios.js +460 -0
  69. package/dist/runner/live-proof-summary.d.ts +263 -0
  70. package/dist/runner/live-proof-summary.js +465 -0
  71. package/dist/runner/live-proof.d.ts +467 -0
  72. package/dist/runner/live-proof.js +920 -0
  73. package/dist/runner/local-env.d.ts +64 -0
  74. package/dist/runner/local-env.js +155 -0
  75. package/dist/runner/profile-android.d.ts +82 -0
  76. package/dist/runner/profile-android.js +671 -0
  77. package/dist/runner/profile-ios.d.ts +108 -0
  78. package/dist/runner/profile-ios.js +532 -0
  79. package/dist/runner/profile-mobile.d.ts +254 -0
  80. package/dist/runner/profile-mobile.js +1307 -0
  81. package/dist/runner/validate-project.d.ts +273 -0
  82. package/dist/runner/validate-project.js +1501 -0
  83. package/docs/adapters.md +145 -0
  84. package/docs/api.md +94 -0
  85. package/docs/authoring.md +196 -0
  86. package/docs/concepts.md +136 -0
  87. package/docs/consumer-rehearsal.md +115 -0
  88. package/docs/contracts.md +267 -0
  89. package/docs/live-proofs.md +270 -0
  90. package/docs/principles.md +46 -0
  91. package/examples/event-logs/app-startup-baseline.log +4 -0
  92. package/examples/event-logs/app-startup-current.log +4 -0
  93. package/examples/minimal-app/README.md +70 -0
  94. package/examples/mobile-app/README.md +302 -0
  95. package/examples/mobile-app/app.json +22 -0
  96. package/examples/mobile-app/asl/package-scripts.json +32 -0
  97. package/examples/mobile-app/asl.config.json +37 -0
  98. package/examples/mobile-app/event-logs/android-app-startup.log +4 -0
  99. package/examples/mobile-app/event-logs/android-open-close-cycle.log +12 -0
  100. package/examples/mobile-app/event-logs/android-scroll-settle.log +12 -0
  101. package/examples/mobile-app/event-logs/app-startup.log +4 -0
  102. package/examples/mobile-app/event-logs/open-close-cycle.log +12 -0
  103. package/examples/mobile-app/event-logs/scroll-settle.log +12 -0
  104. package/examples/mobile-app/index.ts +20 -0
  105. package/examples/mobile-app/metro.config.js +20 -0
  106. package/examples/mobile-app/package.json +62 -0
  107. package/examples/mobile-app/patches/expo-modules-jsi@56.0.10.patch +19 -0
  108. package/examples/mobile-app/plugins/with-ios-build-compat.js +271 -0
  109. package/examples/mobile-app/pnpm-lock.yaml +4440 -0
  110. package/examples/mobile-app/runner-manifests/evidence-provider.json +79 -0
  111. package/examples/mobile-app/runner-manifests/primary-runner.json +19 -0
  112. package/examples/mobile-app/scenarios/android/app-startup-video.json +73 -0
  113. package/examples/mobile-app/scenarios/android/app-startup.json +44 -0
  114. package/examples/mobile-app/scenarios/android/open-close-cycle.json +54 -0
  115. package/examples/mobile-app/scenarios/android/scroll-settle.json +49 -0
  116. package/examples/mobile-app/scenarios/ios/app-startup.json +44 -0
  117. package/examples/mobile-app/scenarios/ios/open-close-cycle.json +54 -0
  118. package/examples/mobile-app/scenarios/ios/scroll-settle.json +49 -0
  119. package/examples/mobile-app/scenarios/mobile/app-startup.json +91 -0
  120. package/examples/mobile-app/scenarios/mobile/open-close-cycle.json +160 -0
  121. package/examples/mobile-app/scenarios/mobile/scroll-settle.json +148 -0
  122. package/examples/mobile-app/scripts/asl-capture-accessibility-provider.mjs +112 -0
  123. package/examples/mobile-app/scripts/asl-capture-profiler-provider.mjs +127 -0
  124. package/examples/mobile-app/src/devtools/profile-session.ts +7 -0
  125. package/examples/mobile-app/src/example-screen.tsx +322 -0
  126. package/examples/mobile-app/tsconfig.json +16 -0
  127. package/examples/mobile-app/tsconfig.typecheck.json +13 -0
  128. package/examples/runners/README.md +44 -0
  129. package/examples/runners/adb-android.json +25 -0
  130. package/examples/runners/agent-device-android.json +27 -0
  131. package/examples/runners/agent-device-ios.json +27 -0
  132. package/examples/runners/argent-android.json +32 -0
  133. package/examples/runners/argent-ios.json +32 -0
  134. package/examples/runners/argent-react-profiler-provider.json +15 -0
  135. package/examples/runners/axe-accessibility-provider.json +24 -0
  136. package/examples/runners/manual-log-ingest.json +9 -0
  137. package/examples/runners/rozenite-profiler-provider.json +9 -0
  138. package/examples/runners/script-accessibility-provider.json +24 -0
  139. package/examples/runners/script-memory-provider.json +24 -0
  140. package/examples/runners/script-network-provider.json +24 -0
  141. package/examples/runners/script-profiler-provider.json +30 -0
  142. package/examples/runners/xcodebuildmcp-ios.json +29 -0
  143. package/examples/scenarios/ios/app-startup.json +28 -0
  144. package/examples/scenarios/ios/open-close-cycle.json +35 -0
  145. package/examples/scenarios/mobile/app-startup.json +72 -0
  146. package/examples/scenarios/mobile/media-open-close.json +141 -0
  147. package/examples/scenarios/mobile/open-close-cycle.json +135 -0
  148. package/examples/scenarios/mobile/scroll-settle.json +106 -0
  149. package/package.json +240 -0
  150. package/schemas/budget-verdict.schema.json +115 -0
  151. package/schemas/causal-run.schema.json +279 -0
  152. package/schemas/comparison.schema.json +196 -0
  153. package/schemas/health.schema.json +108 -0
  154. package/schemas/live-proof-set.schema.json +195 -0
  155. package/schemas/live-proof.schema.json +413 -0
  156. package/schemas/manifest.schema.json +204 -0
  157. package/schemas/metrics.schema.json +137 -0
  158. package/schemas/project-validation.schema.json +343 -0
  159. package/schemas/runner-capabilities.schema.json +217 -0
  160. package/schemas/scenario.schema.json +400 -0
  161. package/schemas/verdict.schema.json +88 -0
  162. package/templates/evidence-provider.json +83 -0
  163. package/templates/gitignore-snippet +9 -0
  164. package/templates/integration-readme.md +125 -0
  165. package/templates/mobile-scenario.json +133 -0
  166. package/templates/package-scripts.json +32 -0
  167. package/templates/primary-runner.json +19 -0
  168. package/templates/project.config.json +37 -0
  169. package/templates/scripts/asl-capture-accessibility-provider.mjs +112 -0
  170. 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, };