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,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
|
+
};
|