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,56 @@
1
+ declare const ARTIFACT_LAYOUT_VERSION = "1.0.0";
2
+ declare const ARTIFACT_FILENAMES: {
3
+ agentSummary: string;
4
+ comparison: string;
5
+ health: string;
6
+ liveProof: string;
7
+ liveProofSet: string;
8
+ plannerCompatibility: string;
9
+ projectValidation: string;
10
+ verdict: string;
11
+ };
12
+ declare const PROFILE_ARTIFACT_FILENAMES: {
13
+ budgetVerdict: string;
14
+ causalRun: string;
15
+ manifest: string;
16
+ metrics: string;
17
+ summary: string;
18
+ };
19
+ type ProfileArtifactPaths = {
20
+ budgetVerdict: string;
21
+ causalRun: string;
22
+ manifest: string;
23
+ metrics: string;
24
+ summary: string;
25
+ };
26
+ type ArtifactLayout = {
27
+ version: string;
28
+ root: string;
29
+ health: string;
30
+ verdict: string;
31
+ comparison: string;
32
+ agentSummary: string;
33
+ liveProof: string;
34
+ liveProofSet: string;
35
+ plannerCompatibility: string;
36
+ projectValidation: string;
37
+ raw: string;
38
+ captures: string;
39
+ signals: {
40
+ js: string;
41
+ memory: string;
42
+ network: string;
43
+ };
44
+ profile: ProfileArtifactPaths;
45
+ };
46
+ /**
47
+ * Builds the stable artifact path contract for one run directory.
48
+ *
49
+ * @param {{outputDir: string}} options
50
+ * @returns {ArtifactLayout}
51
+ */
52
+ declare function createArtifactLayout({ outputDir }: {
53
+ outputDir: string;
54
+ }): ArtifactLayout;
55
+ export { ARTIFACT_FILENAMES, ARTIFACT_LAYOUT_VERSION, PROFILE_ARTIFACT_FILENAMES, createArtifactLayout, };
56
+ export type { ArtifactLayout, ProfileArtifactPaths, };
@@ -0,0 +1,61 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.PROFILE_ARTIFACT_FILENAMES = exports.ARTIFACT_LAYOUT_VERSION = exports.ARTIFACT_FILENAMES = void 0;
4
+ exports.createArtifactLayout = createArtifactLayout;
5
+ const path = require('node:path');
6
+ const ARTIFACT_LAYOUT_VERSION = '1.0.0';
7
+ exports.ARTIFACT_LAYOUT_VERSION = ARTIFACT_LAYOUT_VERSION;
8
+ const ARTIFACT_FILENAMES = {
9
+ agentSummary: 'agent-summary.md',
10
+ comparison: 'comparison.json',
11
+ health: 'health.json',
12
+ liveProof: 'live-proof.json',
13
+ liveProofSet: 'live-proof-set.json',
14
+ plannerCompatibility: 'planner-compatibility.json',
15
+ projectValidation: 'project-validation.json',
16
+ verdict: 'verdict.json',
17
+ };
18
+ exports.ARTIFACT_FILENAMES = ARTIFACT_FILENAMES;
19
+ const PROFILE_ARTIFACT_FILENAMES = {
20
+ budgetVerdict: 'budget-verdict.json',
21
+ causalRun: 'causal-run.json',
22
+ manifest: 'manifest.json',
23
+ metrics: 'metrics.json',
24
+ summary: 'summary.md',
25
+ };
26
+ exports.PROFILE_ARTIFACT_FILENAMES = PROFILE_ARTIFACT_FILENAMES;
27
+ /**
28
+ * Builds the stable artifact path contract for one run directory.
29
+ *
30
+ * @param {{outputDir: string}} options
31
+ * @returns {ArtifactLayout}
32
+ */
33
+ function createArtifactLayout({ outputDir }) {
34
+ const profileArtifacts = {
35
+ budgetVerdict: path.join(outputDir, PROFILE_ARTIFACT_FILENAMES.budgetVerdict),
36
+ causalRun: path.join(outputDir, PROFILE_ARTIFACT_FILENAMES.causalRun),
37
+ manifest: path.join(outputDir, PROFILE_ARTIFACT_FILENAMES.manifest),
38
+ metrics: path.join(outputDir, PROFILE_ARTIFACT_FILENAMES.metrics),
39
+ summary: path.join(outputDir, PROFILE_ARTIFACT_FILENAMES.summary),
40
+ };
41
+ return {
42
+ version: ARTIFACT_LAYOUT_VERSION,
43
+ root: outputDir,
44
+ health: path.join(outputDir, ARTIFACT_FILENAMES.health),
45
+ verdict: path.join(outputDir, ARTIFACT_FILENAMES.verdict),
46
+ comparison: path.join(outputDir, ARTIFACT_FILENAMES.comparison),
47
+ agentSummary: path.join(outputDir, ARTIFACT_FILENAMES.agentSummary),
48
+ liveProof: path.join(outputDir, ARTIFACT_FILENAMES.liveProof),
49
+ liveProofSet: path.join(outputDir, ARTIFACT_FILENAMES.liveProofSet),
50
+ plannerCompatibility: path.join(outputDir, ARTIFACT_FILENAMES.plannerCompatibility),
51
+ projectValidation: path.join(outputDir, ARTIFACT_FILENAMES.projectValidation),
52
+ raw: path.join(outputDir, 'raw'),
53
+ captures: path.join(outputDir, 'captures'),
54
+ signals: {
55
+ js: path.join(outputDir, 'signals', 'js'),
56
+ memory: path.join(outputDir, 'signals', 'memory'),
57
+ network: path.join(outputDir, 'signals', 'network'),
58
+ },
59
+ profile: profileArtifacts,
60
+ };
61
+ }
@@ -0,0 +1,44 @@
1
+ type JsonSchema = Record<string, unknown>;
2
+ /**
3
+ * Writes schema-validated JSON with stable formatting.
4
+ *
5
+ * @param {{filePath: string, value: unknown, schema: Record<string, unknown>, label: string}} options
6
+ * @returns {Promise<string>}
7
+ */
8
+ declare function writeJsonArtifact({ filePath, value, schema, label, }: {
9
+ filePath: string;
10
+ value: unknown;
11
+ schema: JsonSchema;
12
+ label: string;
13
+ }): Promise<string>;
14
+ /**
15
+ * Writes text artifacts with parent-directory creation.
16
+ *
17
+ * @param {{filePath: string, content: string}} options
18
+ * @returns {Promise<string>}
19
+ */
20
+ declare function writeTextArtifact({ filePath, content, }: {
21
+ filePath: string;
22
+ content: string;
23
+ }): Promise<string>;
24
+ /**
25
+ * Copies a raw evidence artifact with parent-directory creation.
26
+ *
27
+ * @param {{sourcePath: string, filePath: string}} options
28
+ * @returns {Promise<string>}
29
+ */
30
+ declare function copyRawArtifact({ sourcePath, filePath, }: {
31
+ sourcePath: string;
32
+ filePath: string;
33
+ }): Promise<string>;
34
+ /**
35
+ * Creates an artifact writer object that satisfies the artifact-writer port.
36
+ *
37
+ * @returns {{writeJson: typeof writeJsonArtifact, writeText: typeof writeTextArtifact, copyRaw: typeof copyRawArtifact}}
38
+ */
39
+ declare function createArtifactWriter(): {
40
+ writeJson: typeof writeJsonArtifact;
41
+ writeText: typeof writeTextArtifact;
42
+ copyRaw: typeof copyRawArtifact;
43
+ };
44
+ export { copyRawArtifact, createArtifactWriter, writeJsonArtifact, writeTextArtifact, };
@@ -0,0 +1,55 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.copyRawArtifact = copyRawArtifact;
4
+ exports.createArtifactWriter = createArtifactWriter;
5
+ exports.writeJsonArtifact = writeJsonArtifact;
6
+ exports.writeTextArtifact = writeTextArtifact;
7
+ const fsp = require('node:fs/promises');
8
+ const path = require('node:path');
9
+ const { assertValidJson } = require('./schema-validator');
10
+ /**
11
+ * Writes schema-validated JSON with stable formatting.
12
+ *
13
+ * @param {{filePath: string, value: unknown, schema: Record<string, unknown>, label: string}} options
14
+ * @returns {Promise<string>}
15
+ */
16
+ async function writeJsonArtifact({ filePath, value, schema, label, }) {
17
+ assertValidJson(value, schema, label);
18
+ await fsp.mkdir(path.dirname(filePath), { recursive: true });
19
+ await fsp.writeFile(filePath, `${JSON.stringify(value, null, 2)}\n`, 'utf8');
20
+ return filePath;
21
+ }
22
+ /**
23
+ * Writes text artifacts with parent-directory creation.
24
+ *
25
+ * @param {{filePath: string, content: string}} options
26
+ * @returns {Promise<string>}
27
+ */
28
+ async function writeTextArtifact({ filePath, content, }) {
29
+ await fsp.mkdir(path.dirname(filePath), { recursive: true });
30
+ await fsp.writeFile(filePath, content, 'utf8');
31
+ return filePath;
32
+ }
33
+ /**
34
+ * Copies a raw evidence artifact with parent-directory creation.
35
+ *
36
+ * @param {{sourcePath: string, filePath: string}} options
37
+ * @returns {Promise<string>}
38
+ */
39
+ async function copyRawArtifact({ sourcePath, filePath, }) {
40
+ await fsp.mkdir(path.dirname(filePath), { recursive: true });
41
+ await fsp.copyFile(sourcePath, filePath);
42
+ return filePath;
43
+ }
44
+ /**
45
+ * Creates an artifact writer object that satisfies the artifact-writer port.
46
+ *
47
+ * @returns {{writeJson: typeof writeJsonArtifact, writeText: typeof writeTextArtifact, copyRaw: typeof copyRawArtifact}}
48
+ */
49
+ function createArtifactWriter() {
50
+ return {
51
+ writeJson: writeJsonArtifact,
52
+ writeText: writeTextArtifact,
53
+ copyRaw: copyRawArtifact,
54
+ };
55
+ }
@@ -0,0 +1,133 @@
1
+ type ComparisonRecord = Record<string, any>;
2
+ type ComparisonBudgetCheck = {
3
+ actual: number | boolean | null;
4
+ name: string;
5
+ pass: boolean;
6
+ unit: 'ms' | 'count' | 'bytes' | 'percent' | 'boolean';
7
+ };
8
+ type MetricComparison = {
9
+ name: string;
10
+ unit: ComparisonBudgetCheck['unit'];
11
+ baseline: number | boolean | null;
12
+ current: number | boolean | null;
13
+ delta: number | null;
14
+ status: 'better' | 'worse' | 'unchanged' | 'inconclusive';
15
+ notes?: string;
16
+ };
17
+ type ComparisonStatus = MetricComparison['status'] | 'mixed';
18
+ type ComparisonBasisStrategy = 'explicit' | 'latest_trusted_prior';
19
+ type ComparisonRunBasis = {
20
+ healthStatus?: string;
21
+ runDir?: string;
22
+ runId: string;
23
+ verdictStatus?: string;
24
+ };
25
+ type ComparisonSelectionBasis = {
26
+ artifactRoot?: string;
27
+ candidatesInspected?: number;
28
+ scenarioId?: string;
29
+ selectedRunDir?: string;
30
+ selectedRunId?: string;
31
+ skippedCurrentRun?: boolean;
32
+ trustedCandidates?: number;
33
+ trustedPriorCandidates?: number;
34
+ };
35
+ type ComparisonBasis = {
36
+ baseline: ComparisonRunBasis;
37
+ current: ComparisonRunBasis;
38
+ selection?: ComparisonSelectionBasis;
39
+ strategy: ComparisonBasisStrategy;
40
+ };
41
+ type BuildComparisonOptions = {
42
+ baselineHealth: ComparisonRecord;
43
+ baselineVerdict: ComparisonRecord;
44
+ comparisonBasis?: ComparisonBasis;
45
+ currentHealth: ComparisonRecord;
46
+ currentVerdict: ComparisonRecord;
47
+ };
48
+ type CompareRunDirectoriesOptions = {
49
+ baselineDir: string;
50
+ currentDir: string;
51
+ selection?: ComparisonSelectionBasis;
52
+ strategy?: ComparisonBasisStrategy;
53
+ };
54
+ /**
55
+ * Reads and validates the health and verdict artifacts from a run directory.
56
+ *
57
+ * @param {string} runDir
58
+ * @returns {{health: Record<string, unknown>, verdict: Record<string, unknown>}}
59
+ */
60
+ declare function readRunArtifacts(runDir: string): {
61
+ health: ComparisonRecord;
62
+ verdict: ComparisonRecord;
63
+ };
64
+ /**
65
+ * Returns budget checks indexed by stable comparison key.
66
+ *
67
+ * @param {unknown} checks
68
+ * @returns {Map<string, BudgetCheck>}
69
+ */
70
+ declare function indexBudgetChecks(checks: unknown): Map<string, ComparisonBudgetCheck>;
71
+ /**
72
+ * Compares one budget check when both runs expose a compatible actual value.
73
+ *
74
+ * @param {BudgetCheck} baseline
75
+ * @param {BudgetCheck} current
76
+ * @returns {MetricComparison}
77
+ */
78
+ declare function compareBudgetCheck(baseline: ComparisonBudgetCheck, current: ComparisonBudgetCheck): MetricComparison;
79
+ /**
80
+ * Collapses metric-level comparison statuses into the run-level comparison status.
81
+ *
82
+ * @param {MetricComparison[]} metricComparisons
83
+ * @param {{baselineVerdictStatus?: unknown, currentVerdictStatus?: unknown}} verdicts
84
+ * @returns {ComparisonStatus}
85
+ */
86
+ declare function resolveComparisonStatus(metricComparisons: MetricComparison[], { baselineVerdictStatus, currentVerdictStatus, }: {
87
+ baselineVerdictStatus?: unknown;
88
+ currentVerdictStatus?: unknown;
89
+ }): ComparisonStatus;
90
+ /**
91
+ * Builds the provenance block that explains which runs a comparison used.
92
+ *
93
+ * @param {{baselineDir: string, currentDir: string, baselineHealth: ComparisonRecord, baselineVerdict: ComparisonRecord, currentHealth: ComparisonRecord, currentVerdict: ComparisonRecord, selection?: ComparisonSelectionBasis, strategy: ComparisonBasisStrategy}} options
94
+ * @returns {ComparisonBasis}
95
+ */
96
+ declare function buildComparisonBasis({ baselineDir, currentDir, baselineHealth, baselineVerdict, currentHealth, currentVerdict, selection, strategy, }: {
97
+ baselineDir: string;
98
+ currentDir: string;
99
+ baselineHealth: ComparisonRecord;
100
+ baselineVerdict: ComparisonRecord;
101
+ currentHealth: ComparisonRecord;
102
+ currentVerdict: ComparisonRecord;
103
+ selection?: ComparisonSelectionBasis;
104
+ strategy: ComparisonBasisStrategy;
105
+ }): ComparisonBasis;
106
+ /**
107
+ * Builds a comparison artifact from two validated run artifact sets.
108
+ *
109
+ * @param {BuildComparisonOptions} options
110
+ * @returns {Record<string, unknown>}
111
+ */
112
+ declare function buildComparisonArtifact({ baselineHealth, baselineVerdict, comparisonBasis, currentHealth, currentVerdict, }: BuildComparisonOptions): ComparisonRecord;
113
+ /**
114
+ * Reads two run directories and builds a validated comparison artifact.
115
+ *
116
+ * @param {CompareRunDirectoriesOptions} options
117
+ * @returns {Record<string, unknown>}
118
+ */
119
+ declare function compareRunDirectories({ baselineDir, currentDir, selection, strategy, }: CompareRunDirectoriesOptions): ComparisonRecord;
120
+ /**
121
+ * Builds the human-readable comparison summary.
122
+ *
123
+ * @param {{comparisonStatus: string, missingRequired: string[], metricComparisons: MetricComparison[], warnings: string[]}} options
124
+ * @returns {string}
125
+ */
126
+ declare function summarizeComparison({ comparisonStatus, missingRequired, metricComparisons, warnings, }: {
127
+ comparisonStatus: string;
128
+ missingRequired: string[];
129
+ metricComparisons: MetricComparison[];
130
+ warnings: string[];
131
+ }): string;
132
+ export { buildComparisonBasis, buildComparisonArtifact, compareBudgetCheck, compareRunDirectories, indexBudgetChecks, readRunArtifacts, resolveComparisonStatus, summarizeComparison, };
133
+ export type { BuildComparisonOptions, ComparisonBasis, ComparisonBasisStrategy, CompareRunDirectoriesOptions, ComparisonBudgetCheck, ComparisonRecord, ComparisonStatus, MetricComparison, };
@@ -0,0 +1,294 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.buildComparisonBasis = buildComparisonBasis;
4
+ exports.buildComparisonArtifact = buildComparisonArtifact;
5
+ exports.compareBudgetCheck = compareBudgetCheck;
6
+ exports.compareRunDirectories = compareRunDirectories;
7
+ exports.indexBudgetChecks = indexBudgetChecks;
8
+ exports.readRunArtifacts = readRunArtifacts;
9
+ exports.resolveComparisonStatus = resolveComparisonStatus;
10
+ exports.summarizeComparison = summarizeComparison;
11
+ const fs = require('node:fs');
12
+ const { createArtifactLayout } = require('./artifact-layout');
13
+ const { SCHEMAS, assertValidJson } = require('./schema-validator');
14
+ const MIN_MS_COMPARISON_TOLERANCE = 16;
15
+ const RELATIVE_MS_COMPARISON_TOLERANCE = 0.05;
16
+ /**
17
+ * Reads and validates the health and verdict artifacts from a run directory.
18
+ *
19
+ * @param {string} runDir
20
+ * @returns {{health: Record<string, unknown>, verdict: Record<string, unknown>}}
21
+ */
22
+ function readRunArtifacts(runDir) {
23
+ const layout = createArtifactLayout({ outputDir: runDir });
24
+ const health = JSON.parse(fs.readFileSync(layout.health, 'utf8'));
25
+ const verdict = JSON.parse(fs.readFileSync(layout.verdict, 'utf8'));
26
+ return {
27
+ health: assertValidJson(health, SCHEMAS.health, 'Health artifact'),
28
+ verdict: assertValidJson(verdict, SCHEMAS.verdict, 'Verdict artifact'),
29
+ };
30
+ }
31
+ /**
32
+ * Returns budget checks indexed by stable comparison key.
33
+ *
34
+ * @param {unknown} checks
35
+ * @returns {Map<string, BudgetCheck>}
36
+ */
37
+ function indexBudgetChecks(checks) {
38
+ const indexed = new Map();
39
+ if (!Array.isArray(checks)) {
40
+ return indexed;
41
+ }
42
+ for (const check of checks) {
43
+ if (!check || typeof check !== 'object') {
44
+ continue;
45
+ }
46
+ const record = check;
47
+ if (typeof record.name !== 'string' || typeof record.unit !== 'string') {
48
+ continue;
49
+ }
50
+ indexed.set(`${record.name}\u0000${record.unit}`, record);
51
+ }
52
+ return indexed;
53
+ }
54
+ /**
55
+ * Returns the absolute delta that should still be treated as timing noise.
56
+ *
57
+ * @param {ComparisonBudgetCheck} baseline
58
+ * @param {ComparisonBudgetCheck} current
59
+ * @returns {number}
60
+ */
61
+ function comparisonTolerance(baseline, current) {
62
+ if (baseline.unit !== 'ms' || current.unit !== 'ms') {
63
+ return 0;
64
+ }
65
+ if (typeof baseline.actual !== 'number' || typeof current.actual !== 'number') {
66
+ return 0;
67
+ }
68
+ const reference = Math.max(Math.abs(baseline.actual), Math.abs(current.actual));
69
+ return Math.max(MIN_MS_COMPARISON_TOLERANCE, reference * RELATIVE_MS_COMPARISON_TOLERANCE);
70
+ }
71
+ /**
72
+ * Compares one budget check when both runs expose a compatible actual value.
73
+ *
74
+ * @param {BudgetCheck} baseline
75
+ * @param {BudgetCheck} current
76
+ * @returns {MetricComparison}
77
+ */
78
+ function compareBudgetCheck(baseline, current) {
79
+ if (typeof baseline.actual !== 'number' || typeof current.actual !== 'number') {
80
+ return {
81
+ name: current.name,
82
+ unit: current.unit,
83
+ baseline: baseline.actual ?? null,
84
+ current: current.actual ?? null,
85
+ delta: null,
86
+ status: baseline.actual === current.actual ? 'unchanged' : 'inconclusive',
87
+ notes: 'Only numeric budget actuals are compared by direction.',
88
+ };
89
+ }
90
+ const delta = current.actual - baseline.actual;
91
+ const tolerance = comparisonTolerance(baseline, current);
92
+ const crossedBudgetBoundary = baseline.pass !== current.pass;
93
+ const withinTolerance = Math.abs(delta) <= tolerance;
94
+ let status;
95
+ if (crossedBudgetBoundary) {
96
+ status = current.pass ? 'better' : 'worse';
97
+ }
98
+ else if (withinTolerance) {
99
+ status = 'unchanged';
100
+ }
101
+ else {
102
+ status = delta < 0 ? 'better' : 'worse';
103
+ }
104
+ return {
105
+ name: current.name,
106
+ unit: current.unit,
107
+ baseline: baseline.actual,
108
+ current: current.actual,
109
+ delta,
110
+ status,
111
+ ...(status === 'unchanged' && delta !== 0 && tolerance > 0 && withinTolerance
112
+ ? { notes: `Delta within ${tolerance}ms timing tolerance.` }
113
+ : {}),
114
+ };
115
+ }
116
+ /**
117
+ * Collapses metric-level comparison statuses into the run-level comparison status.
118
+ *
119
+ * @param {MetricComparison[]} metricComparisons
120
+ * @param {{baselineVerdictStatus?: unknown, currentVerdictStatus?: unknown}} verdicts
121
+ * @returns {ComparisonStatus}
122
+ */
123
+ function resolveComparisonStatus(metricComparisons, { baselineVerdictStatus, currentVerdictStatus, }) {
124
+ const hasBetterMetric = metricComparisons.some((metric) => metric.status === 'better');
125
+ const hasWorseMetric = metricComparisons.some((metric) => metric.status === 'worse');
126
+ if (hasBetterMetric && hasWorseMetric) {
127
+ return 'mixed';
128
+ }
129
+ if (hasWorseMetric) {
130
+ return 'worse';
131
+ }
132
+ if (hasBetterMetric) {
133
+ return 'better';
134
+ }
135
+ if (metricComparisons.length > 0 && metricComparisons.every((metric) => metric.status === 'unchanged')) {
136
+ return 'unchanged';
137
+ }
138
+ if (baselineVerdictStatus === 'failed' && currentVerdictStatus === 'passed') {
139
+ return 'better';
140
+ }
141
+ if (baselineVerdictStatus === 'passed' && currentVerdictStatus === 'failed') {
142
+ return 'worse';
143
+ }
144
+ return 'inconclusive';
145
+ }
146
+ /**
147
+ * Builds the provenance block that explains which runs a comparison used.
148
+ *
149
+ * @param {{baselineDir: string, currentDir: string, baselineHealth: ComparisonRecord, baselineVerdict: ComparisonRecord, currentHealth: ComparisonRecord, currentVerdict: ComparisonRecord, selection?: ComparisonSelectionBasis, strategy: ComparisonBasisStrategy}} options
150
+ * @returns {ComparisonBasis}
151
+ */
152
+ function buildComparisonBasis({ baselineDir, currentDir, baselineHealth, baselineVerdict, currentHealth, currentVerdict, selection, strategy, }) {
153
+ const baselineRunId = String(baselineHealth.runId ?? baselineVerdict.runId ?? 'unknown-baseline');
154
+ const currentRunId = String(currentHealth.runId ?? currentVerdict.runId ?? 'unknown-current');
155
+ return {
156
+ strategy,
157
+ baseline: {
158
+ runId: baselineRunId,
159
+ runDir: baselineDir,
160
+ ...(typeof baselineHealth.healthStatus === 'string' ? { healthStatus: baselineHealth.healthStatus } : {}),
161
+ ...(typeof baselineVerdict.verdictStatus === 'string' ? { verdictStatus: baselineVerdict.verdictStatus } : {}),
162
+ },
163
+ current: {
164
+ runId: currentRunId,
165
+ runDir: currentDir,
166
+ ...(typeof currentHealth.healthStatus === 'string' ? { healthStatus: currentHealth.healthStatus } : {}),
167
+ ...(typeof currentVerdict.verdictStatus === 'string' ? { verdictStatus: currentVerdict.verdictStatus } : {}),
168
+ },
169
+ ...(selection ? { selection } : {}),
170
+ };
171
+ }
172
+ /**
173
+ * Builds a comparison artifact from two validated run artifact sets.
174
+ *
175
+ * @param {BuildComparisonOptions} options
176
+ * @returns {Record<string, unknown>}
177
+ */
178
+ function buildComparisonArtifact({ baselineHealth, baselineVerdict, comparisonBasis, currentHealth, currentVerdict, }) {
179
+ const missingRequired = [];
180
+ const warnings = [];
181
+ const baselineScenarioId = String(baselineHealth.scenarioId ?? baselineVerdict.scenarioId ?? 'unknown-scenario');
182
+ const currentScenarioId = String(currentHealth.scenarioId ?? currentVerdict.scenarioId ?? baselineScenarioId);
183
+ const baselineRunId = String(baselineHealth.runId ?? baselineVerdict.runId ?? 'unknown-baseline');
184
+ const currentRunId = String(currentHealth.runId ?? currentVerdict.runId ?? 'unknown-current');
185
+ if (baselineHealth.healthStatus !== 'passed') {
186
+ missingRequired.push('baseline health passed');
187
+ }
188
+ if (currentHealth.healthStatus !== 'passed') {
189
+ missingRequired.push('current health passed');
190
+ }
191
+ if (baselineScenarioId !== currentScenarioId) {
192
+ missingRequired.push('matching scenario id');
193
+ }
194
+ const canCompare = missingRequired.length === 0;
195
+ const metricComparisons = [];
196
+ if (canCompare) {
197
+ const baselineChecks = indexBudgetChecks(baselineVerdict.budgetChecks);
198
+ const currentChecks = indexBudgetChecks(currentVerdict.budgetChecks);
199
+ for (const [key, currentCheck] of currentChecks.entries()) {
200
+ const baselineCheck = baselineChecks.get(key);
201
+ if (!baselineCheck) {
202
+ warnings.push(`No baseline budget check matched ${currentCheck.name}.`);
203
+ continue;
204
+ }
205
+ metricComparisons.push(compareBudgetCheck(baselineCheck, currentCheck));
206
+ }
207
+ if (metricComparisons.length === 0) {
208
+ warnings.push('No comparable budget checks were available.');
209
+ }
210
+ }
211
+ const comparisonStatus = canCompare
212
+ ? resolveComparisonStatus(metricComparisons, {
213
+ baselineVerdictStatus: baselineVerdict.verdictStatus,
214
+ currentVerdictStatus: currentVerdict.verdictStatus,
215
+ })
216
+ : 'inconclusive';
217
+ const comparison = {
218
+ schemaVersion: '1.0.0',
219
+ scenarioId: currentScenarioId,
220
+ ...(typeof currentHealth.flowId === 'string'
221
+ ? { flowId: currentHealth.flowId }
222
+ : typeof currentVerdict.flowId === 'string'
223
+ ? { flowId: currentVerdict.flowId }
224
+ : {}),
225
+ runId: currentRunId,
226
+ baselineRunId,
227
+ comparisonStatus,
228
+ healthStatus: canCompare ? 'passed' : 'failed',
229
+ verdictStatus: typeof currentVerdict.verdictStatus === 'string' ? currentVerdict.verdictStatus : 'inconclusive',
230
+ ...(comparisonBasis ? { comparisonBasis } : {}),
231
+ ...(metricComparisons.length > 0 ? { metricComparisons } : {}),
232
+ evidence: {
233
+ missingRequired,
234
+ warnings,
235
+ },
236
+ summary: summarizeComparison({ comparisonStatus, missingRequired, metricComparisons, warnings }),
237
+ };
238
+ return assertValidJson(comparison, SCHEMAS.comparison, 'Comparison artifact');
239
+ }
240
+ /**
241
+ * Reads two run directories and builds a validated comparison artifact.
242
+ *
243
+ * @param {CompareRunDirectoriesOptions} options
244
+ * @returns {Record<string, unknown>}
245
+ */
246
+ function compareRunDirectories({ baselineDir, currentDir, selection, strategy = 'explicit', }) {
247
+ const baseline = readRunArtifacts(baselineDir);
248
+ const current = readRunArtifacts(currentDir);
249
+ return buildComparisonArtifact({
250
+ baselineHealth: baseline.health,
251
+ baselineVerdict: baseline.verdict,
252
+ comparisonBasis: buildComparisonBasis({
253
+ baselineDir,
254
+ currentDir,
255
+ baselineHealth: baseline.health,
256
+ baselineVerdict: baseline.verdict,
257
+ currentHealth: current.health,
258
+ currentVerdict: current.verdict,
259
+ ...(selection ? { selection } : {}),
260
+ strategy,
261
+ }),
262
+ currentHealth: current.health,
263
+ currentVerdict: current.verdict,
264
+ });
265
+ }
266
+ /**
267
+ * Builds the human-readable comparison summary.
268
+ *
269
+ * @param {{comparisonStatus: string, missingRequired: string[], metricComparisons: MetricComparison[], warnings: string[]}} options
270
+ * @returns {string}
271
+ */
272
+ function summarizeComparison({ comparisonStatus, missingRequired, metricComparisons, warnings, }) {
273
+ if (missingRequired.length > 0) {
274
+ return `Comparison is inconclusive because required evidence is missing: ${missingRequired.join(', ')}.`;
275
+ }
276
+ if (metricComparisons.length === 0) {
277
+ return warnings.length > 0
278
+ ? `Comparison is inconclusive: ${warnings.join(' ')}`
279
+ : 'Comparison is inconclusive because there were no comparable metrics.';
280
+ }
281
+ if (comparisonStatus === 'better') {
282
+ return 'Current run improved against the explicit baseline.';
283
+ }
284
+ if (comparisonStatus === 'worse') {
285
+ return 'Current run regressed against the explicit baseline.';
286
+ }
287
+ if (comparisonStatus === 'mixed') {
288
+ return 'Current run has mixed metric movement against the explicit baseline.';
289
+ }
290
+ if (comparisonStatus === 'unchanged') {
291
+ return 'Current run matched the explicit baseline.';
292
+ }
293
+ return 'Comparison is inconclusive.';
294
+ }
@@ -0,0 +1,28 @@
1
+ /**
2
+ * Returns whether timing evidence can be used for interpretation.
3
+ *
4
+ * @param {Record<string, unknown>} health
5
+ * @returns {boolean}
6
+ */
7
+ declare function isTimingEvidenceTrusted(health: EvidenceRecord): boolean;
8
+ /**
9
+ * Builds evidence-backed interpretation hints without overriding scenario health.
10
+ *
11
+ * Timing-based hints are only emitted after scenario health passed.
12
+ *
13
+ * @param {{health: Record<string, unknown>, verdict?: Record<string, unknown> | null, comparison?: Record<string, unknown> | null}} options
14
+ * @returns {{timingTrusted: boolean, recommendations: string[], blockedReasons: string[]}}
15
+ */
16
+ declare function interpretEvidence({ health, verdict, comparison, }: {
17
+ health: EvidenceRecord;
18
+ verdict?: EvidenceRecord | null;
19
+ comparison?: EvidenceRecord | null;
20
+ }): EvidenceInterpretation;
21
+ export { interpretEvidence, isTimingEvidenceTrusted, };
22
+ export type { EvidenceInterpretation, EvidenceRecord, };
23
+ type EvidenceRecord = Record<string, unknown>;
24
+ type EvidenceInterpretation = {
25
+ timingTrusted: boolean;
26
+ recommendations: string[];
27
+ blockedReasons: string[];
28
+ };