@alisio/core 0.5.2 → 0.7.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/dist/analysis/janitor.d.ts +48 -0
- package/dist/analysis/janitor.js +170 -18
- package/dist/analysis/jobs.d.ts +11 -2
- package/dist/analysis/jobs.js +13 -0
- package/dist/analysis/oci.d.ts +11 -2
- package/dist/analysis/oci.js +20 -4
- package/dist/analysis/python/sources.js +2 -1
- package/dist/analysis/rerun.d.ts +1 -1
- package/dist/analysis/status.d.ts +3 -1
- package/dist/analysis/status.js +3 -0
- package/dist/application.d.ts +28 -2
- package/dist/application.js +189 -4
- package/dist/changelog/data.js +42 -0
- package/dist/config.d.ts +110 -2
- package/dist/config.js +188 -6
- package/dist/core/call-internals.d.ts +22 -0
- package/dist/core/call-internals.js +9 -0
- package/dist/core/contracts.d.ts +2 -2
- package/dist/core/runner.js +111 -38
- package/dist/eval/canonical.d.ts +14 -0
- package/dist/eval/canonical.js +40 -0
- package/dist/eval/cassette.d.ts +180 -0
- package/dist/eval/cassette.js +424 -0
- package/dist/eval/compare.d.ts +147 -0
- package/dist/eval/compare.js +436 -0
- package/dist/eval/engine.d.ts +144 -0
- package/dist/eval/engine.js +728 -0
- package/dist/eval/export.d.ts +40 -0
- package/dist/eval/export.js +90 -0
- package/dist/eval/fingerprint.d.ts +88 -0
- package/dist/eval/fingerprint.js +186 -0
- package/dist/eval/normalize.d.ts +68 -0
- package/dist/eval/normalize.js +138 -0
- package/dist/eval/paths.d.ts +11 -0
- package/dist/eval/paths.js +50 -0
- package/dist/eval/plan.d.ts +26 -0
- package/dist/eval/plan.js +44 -0
- package/dist/eval/report.d.ts +53 -0
- package/dist/eval/report.js +258 -0
- package/dist/eval/run-suite.d.ts +68 -0
- package/dist/eval/run-suite.js +94 -0
- package/dist/eval/secrets.d.ts +12 -0
- package/dist/eval/secrets.js +45 -0
- package/dist/eval/stats.d.ts +17 -0
- package/dist/eval/stats.js +50 -0
- package/dist/eval/suite.d.ts +20 -0
- package/dist/eval/suite.js +165 -0
- package/dist/eval/variants.d.ts +35 -0
- package/dist/eval/variants.js +137 -0
- package/dist/eval/verifiers.d.ts +49 -0
- package/dist/eval/verifiers.js +264 -0
- package/dist/forge/broker.d.ts +51 -0
- package/dist/forge/broker.js +244 -0
- package/dist/forge/budget.d.ts +27 -0
- package/dist/forge/budget.js +60 -0
- package/dist/forge/catalog-view.d.ts +24 -0
- package/dist/forge/catalog-view.js +43 -0
- package/dist/forge/errors.d.ts +26 -0
- package/dist/forge/errors.js +49 -0
- package/dist/forge/guides/javascript.d.ts +25 -0
- package/dist/forge/guides/javascript.js +90 -0
- package/dist/forge/guides/provider.d.ts +42 -0
- package/dist/forge/guides/provider.js +96 -0
- package/dist/forge/guides/python-environment.d.ts +15 -0
- package/dist/forge/guides/python-environment.js +81 -0
- package/dist/forge/guides/python.d.ts +38 -0
- package/dist/forge/guides/python.js +170 -0
- package/dist/forge/javascript/engine.d.ts +44 -0
- package/dist/forge/javascript/engine.js +229 -0
- package/dist/forge/javascript/hosts/factory.d.ts +25 -0
- package/dist/forge/javascript/hosts/factory.js +21 -0
- package/dist/forge/javascript/hosts/framing.d.ts +17 -0
- package/dist/forge/javascript/hosts/framing.js +49 -0
- package/dist/forge/javascript/hosts/isolate-main.d.ts +9 -0
- package/dist/forge/javascript/hosts/isolate-main.js +63 -0
- package/dist/forge/javascript/hosts/process-entry.d.ts +1 -0
- package/dist/forge/javascript/hosts/process-entry.js +3 -0
- package/dist/forge/javascript/hosts/process-host.d.ts +33 -0
- package/dist/forge/javascript/hosts/process-host.js +160 -0
- package/dist/forge/javascript/hosts/worker-entry.d.ts +1 -0
- package/dist/forge/javascript/hosts/worker-entry.js +3 -0
- package/dist/forge/javascript/hosts/worker-host.d.ts +27 -0
- package/dist/forge/javascript/hosts/worker-host.js +116 -0
- package/dist/forge/javascript/prelude.d.ts +2 -0
- package/dist/forge/javascript/prelude.js +333 -0
- package/dist/forge/javascript/probe.d.ts +2 -0
- package/dist/forge/javascript/probe.js +53 -0
- package/dist/forge/javascript/runtime-manifest.d.ts +14 -0
- package/dist/forge/javascript/runtime-manifest.js +14 -0
- package/dist/forge/javascript/vm-runner.d.ts +21 -0
- package/dist/forge/javascript/vm-runner.js +301 -0
- package/dist/forge/javascript/wasm-asset.d.ts +8 -0
- package/dist/forge/javascript/wasm-asset.js +8 -0
- package/dist/forge/javascript/wasm.d.ts +15 -0
- package/dist/forge/javascript/wasm.js +54 -0
- package/dist/forge/journal.d.ts +105 -0
- package/dist/forge/journal.js +174 -0
- package/dist/forge/kernel.d.ts +76 -0
- package/dist/forge/kernel.js +339 -0
- package/dist/forge/limits.d.ts +38 -0
- package/dist/forge/limits.js +73 -0
- package/dist/forge/nested.d.ts +96 -0
- package/dist/forge/nested.js +214 -0
- package/dist/forge/ports.d.ts +120 -0
- package/dist/forge/ports.js +1 -0
- package/dist/forge/progress.d.ts +24 -0
- package/dist/forge/progress.js +76 -0
- package/dist/forge/protocol.d.ts +110 -0
- package/dist/forge/protocol.js +171 -0
- package/dist/forge/python/backend-selector.d.ts +38 -0
- package/dist/forge/python/backend-selector.js +111 -0
- package/dist/forge/python/backend.d.ts +89 -0
- package/dist/forge/python/backend.js +1 -0
- package/dist/forge/python/engine.d.ts +84 -0
- package/dist/forge/python/engine.js +279 -0
- package/dist/forge/python/frame-parser.d.ts +24 -0
- package/dist/forge/python/frame-parser.js +107 -0
- package/dist/forge/python/frame-transport.d.ts +40 -0
- package/dist/forge/python/frame-transport.js +101 -0
- package/dist/forge/python/import-scan.d.ts +22 -0
- package/dist/forge/python/import-scan.js +133 -0
- package/dist/forge/python/managed-backend.d.ts +30 -0
- package/dist/forge/python/managed-backend.js +111 -0
- package/dist/forge/python/oci-backend.d.ts +27 -0
- package/dist/forge/python/oci-backend.js +84 -0
- package/dist/forge/python/pyodide-backend.d.ts +40 -0
- package/dist/forge/python/pyodide-backend.js +289 -0
- package/dist/forge/python/pyodide-install.d.ts +41 -0
- package/dist/forge/python/pyodide-install.js +167 -0
- package/dist/forge/python/pyodide-manifest.d.ts +42 -0
- package/dist/forge/python/pyodide-manifest.js +376 -0
- package/dist/forge/python/pyodide-runner-source.d.ts +2 -0
- package/dist/forge/python/pyodide-runner-source.js +3 -0
- package/dist/forge/python/rss.d.ts +2 -0
- package/dist/forge/python/rss.js +33 -0
- package/dist/forge/python/scratch-copy.d.ts +6 -0
- package/dist/forge/python/scratch-copy.js +61 -0
- package/dist/forge/python/settle.d.ts +6 -0
- package/dist/forge/python/settle.js +21 -0
- package/dist/forge/python/status.d.ts +8 -0
- package/dist/forge/python/status.js +15 -0
- package/dist/forge/python/wasm-node.d.ts +59 -0
- package/dist/forge/python/wasm-node.js +220 -0
- package/dist/forge/python/wiring.d.ts +39 -0
- package/dist/forge/python/wiring.js +47 -0
- package/dist/forge/registry.d.ts +26 -0
- package/dist/forge/registry.js +83 -0
- package/dist/forge/result.d.ts +58 -0
- package/dist/forge/result.js +86 -0
- package/dist/forge/routing-rule.d.ts +19 -0
- package/dist/forge/routing-rule.js +31 -0
- package/dist/forge/runtime.d.ts +64 -0
- package/dist/forge/runtime.js +88 -0
- package/dist/forge/scratch-files.d.ts +36 -0
- package/dist/forge/scratch-files.js +94 -0
- package/dist/forge/scratch-provider.d.ts +41 -0
- package/dist/forge/scratch-provider.js +79 -0
- package/dist/forge/scratch-quota.d.ts +8 -0
- package/dist/forge/scratch-quota.js +25 -0
- package/dist/forge/selector.d.ts +35 -0
- package/dist/forge/selector.js +51 -0
- package/dist/forge/session-dir.d.ts +59 -0
- package/dist/forge/session-dir.js +239 -0
- package/dist/forge/session-path.d.ts +31 -0
- package/dist/forge/session-path.js +61 -0
- package/dist/forge/slots.d.ts +16 -0
- package/dist/forge/slots.js +47 -0
- package/dist/forge/state-delta.d.ts +54 -0
- package/dist/forge/state-delta.js +99 -0
- package/dist/forge/state-machine.d.ts +20 -0
- package/dist/forge/state-machine.js +40 -0
- package/dist/forge/state-store.d.ts +25 -0
- package/dist/forge/state-store.js +50 -0
- package/dist/forge/task-log.d.ts +9 -0
- package/dist/forge/task-log.js +42 -0
- package/dist/forge/verdict.d.ts +20 -0
- package/dist/forge/verdict.js +28 -0
- package/dist/forge-isolate.d.ts +6 -0
- package/dist/forge-isolate.js +6 -0
- package/dist/index.d.ts +23 -1
- package/dist/index.js +23 -1
- package/dist/permissions/decide.d.ts +42 -0
- package/dist/permissions/decide.js +24 -0
- package/dist/plan/exit-plan.js +2 -1
- package/dist/resources/context.d.ts +2 -1
- package/dist/resources/context.js +2 -1
- package/dist/runtime/access.d.ts +34 -0
- package/dist/runtime/access.js +80 -0
- package/dist/runtime/ids.d.ts +21 -0
- package/dist/runtime/ids.js +41 -1
- package/dist/runtime/process.d.ts +2 -1
- package/dist/runtime/process.js +5 -5
- package/dist/runtime/store.d.ts +11 -0
- package/dist/runtime/store.js +106 -2
- package/dist/tools/analysis.d.ts +25 -2
- package/dist/tools/analysis.js +315 -267
- package/dist/tools/execute.js +6 -7
- package/dist/tools/forge-guide.d.ts +6 -0
- package/dist/tools/forge-guide.js +60 -0
- package/dist/tools/js-run.d.ts +19 -0
- package/dist/tools/js-run.js +126 -0
- package/dist/tools/standard.d.ts +7 -1
- package/dist/tools/standard.js +44 -43
- package/package.json +7 -2
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Comparison of two reliability reports and of config variants (specs/010 D7, D14, D15, O3).
|
|
3
|
+
* Pure: no I/O.
|
|
4
|
+
*
|
|
5
|
+
* Statistical rules (the review of Phase 3 starts here):
|
|
6
|
+
* - Only reports of the same `suiteId` AND `suiteVersion` are comparable (typed error otherwise).
|
|
7
|
+
* - Success rates are compared through their 95 % Wilson intervals. The comparison is
|
|
8
|
+
* `inconclusive` when the closed intervals overlap (touching counts as overlap) and `low_power`
|
|
9
|
+
* when either side has fewer than 5 judged runs (`MIN_POWER_K`) even if the intervals are
|
|
10
|
+
* disjoint. `improved` / `regressed` appear only when both sides have k >= 5 and the intervals
|
|
11
|
+
* are disjoint. This is a conservative non-overlap rule, not a hypothesis test: it is stricter
|
|
12
|
+
* than a 5 % test, so it misses real differences instead of inventing them. No multiplicity
|
|
13
|
+
* correction is applied across tasks.
|
|
14
|
+
* - No direction is claimed when the comparison is not like for like: either side has errored runs
|
|
15
|
+
* (the judged runs are then a filtered sample), the modes differ, or (replay) the repetitions
|
|
16
|
+
* are identical so an interval means nothing; replay reports get `changed` / `unchanged` only.
|
|
17
|
+
* - Token and cost deltas are refused when the compaction settings differ: compaction calls are
|
|
18
|
+
* not in the measured tokens, so the two sides would not count the same work.
|
|
19
|
+
* - Nothing is paired: the seed only orders the run plan and never reaches the provider, so the
|
|
20
|
+
* two sides are independent samples and the unpaired intervals are the model.
|
|
21
|
+
* - Every other measured metric gets a plain delta (candidate - baseline) with no direction label:
|
|
22
|
+
* they have no interval here, so no claim is made about them.
|
|
23
|
+
* - Deltas are computed inside `measured` and inside `estimated` and never across them. Costs are
|
|
24
|
+
* compared only when both reports carry the same rates.
|
|
25
|
+
*/
|
|
26
|
+
import type { EvalReport } from "@alisio/sdk";
|
|
27
|
+
export declare class EvalCompareError extends Error {
|
|
28
|
+
readonly reason: "suite_id" | "suite_version";
|
|
29
|
+
constructor(message: string, reason: "suite_id" | "suite_version");
|
|
30
|
+
}
|
|
31
|
+
export type Conditions = EvalReport["conditions"];
|
|
32
|
+
export type Verdict = "improved" | "regressed" | "inconclusive" | "low_power" | "not comparable: errored runs" | "not comparable: mode differs"
|
|
33
|
+
/** Replay only: the pass status or a measured count differs / does not. */
|
|
34
|
+
| "changed" | "unchanged";
|
|
35
|
+
export type TokenComparison = "comparable" | "not comparable: compaction settings differ";
|
|
36
|
+
export interface MetricDelta {
|
|
37
|
+
baseline: number;
|
|
38
|
+
candidate: number;
|
|
39
|
+
/** candidate - baseline */
|
|
40
|
+
delta: number;
|
|
41
|
+
}
|
|
42
|
+
export interface Interval {
|
|
43
|
+
value: number;
|
|
44
|
+
/** Absent for replay reports: identical repetitions give an interval no meaning. */
|
|
45
|
+
wilsonLow?: number;
|
|
46
|
+
wilsonHigh?: number;
|
|
47
|
+
}
|
|
48
|
+
declare const CHANGED_FIELDS: readonly ["configHash", "model", "harnessVersion", "mode", "provider"];
|
|
49
|
+
declare const MEASURED_KEYS: readonly ["medianWallMs", "varianceWallMs", "varianceTokens", "tokensPerSuccess", "wallMsPerSuccess"];
|
|
50
|
+
type MeasuredKey = (typeof MEASURED_KEYS)[number];
|
|
51
|
+
export interface TaskComparison {
|
|
52
|
+
taskId: string;
|
|
53
|
+
/** Judged runs on each side. */
|
|
54
|
+
k: {
|
|
55
|
+
baseline: number;
|
|
56
|
+
candidate: number;
|
|
57
|
+
};
|
|
58
|
+
/** Either side has fewer than 5 judged runs. */
|
|
59
|
+
lowPower: boolean;
|
|
60
|
+
/** The judged-run counts differ, so pass^k is not comparable between the two sides. */
|
|
61
|
+
kMismatch: boolean;
|
|
62
|
+
/** Runs excluded as infrastructure errors on each side. */
|
|
63
|
+
errored: {
|
|
64
|
+
baseline: number;
|
|
65
|
+
candidate: number;
|
|
66
|
+
};
|
|
67
|
+
successRate: {
|
|
68
|
+
baseline: Interval;
|
|
69
|
+
candidate: Interval;
|
|
70
|
+
delta: number;
|
|
71
|
+
/** Absent when no interval was compared (replay, or a mode mismatch). */
|
|
72
|
+
overlap?: boolean;
|
|
73
|
+
verdict: Verdict;
|
|
74
|
+
};
|
|
75
|
+
/** Only for two replay reports: deterministic changed/unchanged facts. */
|
|
76
|
+
replay?: {
|
|
77
|
+
baselinePass: boolean;
|
|
78
|
+
candidatePass: boolean;
|
|
79
|
+
passChanged: boolean;
|
|
80
|
+
countsChanged: boolean;
|
|
81
|
+
};
|
|
82
|
+
passAtK: {
|
|
83
|
+
baseline: boolean;
|
|
84
|
+
candidate: boolean;
|
|
85
|
+
};
|
|
86
|
+
passPowK: {
|
|
87
|
+
baseline: boolean;
|
|
88
|
+
candidate: boolean;
|
|
89
|
+
};
|
|
90
|
+
/** Only the metrics both sides could compute. */
|
|
91
|
+
measured: Partial<Record<MeasuredKey, MetricDelta>>;
|
|
92
|
+
/** Present only when both reports carry the same rates and both could compute the cost. */
|
|
93
|
+
estimated?: {
|
|
94
|
+
costUsdPerSuccess?: MetricDelta;
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
export interface EvalComparison {
|
|
98
|
+
schemaVersion: 1;
|
|
99
|
+
suiteId: string;
|
|
100
|
+
suiteVersion: string;
|
|
101
|
+
baseline: Conditions;
|
|
102
|
+
candidate: Conditions;
|
|
103
|
+
/** Conditions that differ between the two reports (`configHash` over the SHARED tasks only). */
|
|
104
|
+
changed: Array<{
|
|
105
|
+
field: (typeof CHANGED_FIELDS)[number];
|
|
106
|
+
baseline: string;
|
|
107
|
+
candidate: string;
|
|
108
|
+
}>;
|
|
109
|
+
/** Runs excluded as infrastructure errors on each side (every task, shared or not). */
|
|
110
|
+
errored: {
|
|
111
|
+
baseline: number;
|
|
112
|
+
candidate: number;
|
|
113
|
+
};
|
|
114
|
+
/** Whether token and cost deltas were computed (they need equal compaction settings). */
|
|
115
|
+
tokenComparison: TokenComparison;
|
|
116
|
+
tasks: TaskComparison[];
|
|
117
|
+
onlyInBaseline: string[];
|
|
118
|
+
onlyInCandidate: string[];
|
|
119
|
+
warnings: string[];
|
|
120
|
+
}
|
|
121
|
+
/** Compares `candidate` against `baseline`; throws `EvalCompareError` for different suites. */
|
|
122
|
+
export declare function compareReports(baseline: EvalReport, candidate: EvalReport): EvalComparison;
|
|
123
|
+
export interface VariantComparison {
|
|
124
|
+
suiteId: string;
|
|
125
|
+
suiteVersion: string;
|
|
126
|
+
baselineId: string;
|
|
127
|
+
/** The config overrides each variant ran with (what the variants file declared). */
|
|
128
|
+
baselineOverrides: Record<string, unknown>;
|
|
129
|
+
variants: Array<{
|
|
130
|
+
id: string;
|
|
131
|
+
overrides: Record<string, unknown>;
|
|
132
|
+
comparison: EvalComparison;
|
|
133
|
+
}>;
|
|
134
|
+
}
|
|
135
|
+
/** Compares every variant after the first against the first (the baseline). */
|
|
136
|
+
export declare function compareVariants(variants: Array<{
|
|
137
|
+
id: string;
|
|
138
|
+
report: EvalReport;
|
|
139
|
+
overrides?: Record<string, unknown>;
|
|
140
|
+
}>): VariantComparison;
|
|
141
|
+
/** Validates the parsed JSON of a `report.json`; throws an actionable `Error` otherwise. */
|
|
142
|
+
export declare function parseReport(value: unknown): EvalReport;
|
|
143
|
+
/** Markdown of a comparison: what changed, verdicts, then Measured and Estimated apart. */
|
|
144
|
+
export declare function renderComparisonMarkdown(c: EvalComparison): string;
|
|
145
|
+
/** Markdown of a variants comparison: one section per candidate variant against the baseline. */
|
|
146
|
+
export declare function renderVariantComparisonMarkdown(v: VariantComparison): string;
|
|
147
|
+
export {};
|
|
@@ -0,0 +1,436 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Comparison of two reliability reports and of config variants (specs/010 D7, D14, D15, O3).
|
|
3
|
+
* Pure: no I/O.
|
|
4
|
+
*
|
|
5
|
+
* Statistical rules (the review of Phase 3 starts here):
|
|
6
|
+
* - Only reports of the same `suiteId` AND `suiteVersion` are comparable (typed error otherwise).
|
|
7
|
+
* - Success rates are compared through their 95 % Wilson intervals. The comparison is
|
|
8
|
+
* `inconclusive` when the closed intervals overlap (touching counts as overlap) and `low_power`
|
|
9
|
+
* when either side has fewer than 5 judged runs (`MIN_POWER_K`) even if the intervals are
|
|
10
|
+
* disjoint. `improved` / `regressed` appear only when both sides have k >= 5 and the intervals
|
|
11
|
+
* are disjoint. This is a conservative non-overlap rule, not a hypothesis test: it is stricter
|
|
12
|
+
* than a 5 % test, so it misses real differences instead of inventing them. No multiplicity
|
|
13
|
+
* correction is applied across tasks.
|
|
14
|
+
* - No direction is claimed when the comparison is not like for like: either side has errored runs
|
|
15
|
+
* (the judged runs are then a filtered sample), the modes differ, or (replay) the repetitions
|
|
16
|
+
* are identical so an interval means nothing; replay reports get `changed` / `unchanged` only.
|
|
17
|
+
* - Token and cost deltas are refused when the compaction settings differ: compaction calls are
|
|
18
|
+
* not in the measured tokens, so the two sides would not count the same work.
|
|
19
|
+
* - Nothing is paired: the seed only orders the run plan and never reaches the provider, so the
|
|
20
|
+
* two sides are independent samples and the unpaired intervals are the model.
|
|
21
|
+
* - Every other measured metric gets a plain delta (candidate - baseline) with no direction label:
|
|
22
|
+
* they have no interval here, so no claim is made about them.
|
|
23
|
+
* - Deltas are computed inside `measured` and inside `estimated` and never across them. Costs are
|
|
24
|
+
* compared only when both reports carry the same rates.
|
|
25
|
+
*/
|
|
26
|
+
import { createHash } from "node:crypto";
|
|
27
|
+
import { z } from "zod";
|
|
28
|
+
import { MIN_POWER_K } from "./report.js";
|
|
29
|
+
export class EvalCompareError extends Error {
|
|
30
|
+
reason;
|
|
31
|
+
constructor(message, reason) {
|
|
32
|
+
super(message);
|
|
33
|
+
this.reason = reason;
|
|
34
|
+
this.name = "EvalCompareError";
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
const CHANGED_FIELDS = ["configHash", "model", "harnessVersion", "mode", "provider"];
|
|
38
|
+
const MEASURED_KEYS = [
|
|
39
|
+
"medianWallMs",
|
|
40
|
+
"varianceWallMs",
|
|
41
|
+
"varianceTokens",
|
|
42
|
+
"tokensPerSuccess",
|
|
43
|
+
"wallMsPerSuccess",
|
|
44
|
+
];
|
|
45
|
+
const delta = (baseline, candidate) => ({
|
|
46
|
+
baseline,
|
|
47
|
+
candidate,
|
|
48
|
+
delta: candidate - baseline,
|
|
49
|
+
});
|
|
50
|
+
const TOKEN_KEYS = ["varianceTokens", "tokensPerSuccess"];
|
|
51
|
+
function compareTask(a, b, ctx) {
|
|
52
|
+
const lowPower = a.k < MIN_POWER_K || b.k < MIN_POWER_K;
|
|
53
|
+
const difference = b.successRate.value - a.successRate.value;
|
|
54
|
+
const erroredA = a.errored ?? 0;
|
|
55
|
+
const erroredB = b.errored ?? 0;
|
|
56
|
+
const measured = {};
|
|
57
|
+
for (const key of MEASURED_KEYS) {
|
|
58
|
+
if (!ctx.tokensComparable && TOKEN_KEYS.includes(key))
|
|
59
|
+
continue;
|
|
60
|
+
const before = a.measured[key];
|
|
61
|
+
const after = b.measured[key];
|
|
62
|
+
if (before !== undefined && after !== undefined)
|
|
63
|
+
measured[key] = delta(before, after);
|
|
64
|
+
}
|
|
65
|
+
const costBefore = a.estimated?.costUsdPerSuccess;
|
|
66
|
+
const costAfter = b.estimated?.costUsdPerSuccess;
|
|
67
|
+
const replayFacts = ctx.replay && !ctx.modeDiffers
|
|
68
|
+
? {
|
|
69
|
+
baselinePass: a.passPowK,
|
|
70
|
+
candidatePass: b.passPowK,
|
|
71
|
+
passChanged: a.passPowK !== b.passPowK || difference !== 0,
|
|
72
|
+
countsChanged: ctx.tokensComparable &&
|
|
73
|
+
(a.measured.tokensPerSuccess !== b.measured.tokensPerSuccess ||
|
|
74
|
+
a.measured.varianceTokens !== b.measured.varianceTokens),
|
|
75
|
+
}
|
|
76
|
+
: undefined;
|
|
77
|
+
let overlap;
|
|
78
|
+
let verdict;
|
|
79
|
+
const withWilson = !replayFacts;
|
|
80
|
+
if (withWilson)
|
|
81
|
+
overlap =
|
|
82
|
+
a.successRate.wilsonLow <= b.successRate.wilsonHigh &&
|
|
83
|
+
b.successRate.wilsonLow <= a.successRate.wilsonHigh;
|
|
84
|
+
if (ctx.modeDiffers)
|
|
85
|
+
verdict = "not comparable: mode differs";
|
|
86
|
+
else if (erroredA > 0 || erroredB > 0)
|
|
87
|
+
verdict = "not comparable: errored runs";
|
|
88
|
+
else if (replayFacts)
|
|
89
|
+
verdict = replayFacts.passChanged || replayFacts.countsChanged ? "changed" : "unchanged";
|
|
90
|
+
else if (overlap)
|
|
91
|
+
verdict = "inconclusive";
|
|
92
|
+
else if (lowPower)
|
|
93
|
+
verdict = "low_power";
|
|
94
|
+
else
|
|
95
|
+
verdict = difference > 0 ? "improved" : "regressed";
|
|
96
|
+
const side = (m) => withWilson ? { ...m.successRate } : { value: m.successRate.value };
|
|
97
|
+
return {
|
|
98
|
+
taskId: a.taskId,
|
|
99
|
+
k: { baseline: a.k, candidate: b.k },
|
|
100
|
+
lowPower,
|
|
101
|
+
kMismatch: a.k !== b.k,
|
|
102
|
+
errored: { baseline: erroredA, candidate: erroredB },
|
|
103
|
+
successRate: {
|
|
104
|
+
baseline: side(a),
|
|
105
|
+
candidate: side(b),
|
|
106
|
+
delta: difference,
|
|
107
|
+
...(overlap === undefined ? {} : { overlap }),
|
|
108
|
+
verdict,
|
|
109
|
+
},
|
|
110
|
+
...(replayFacts ? { replay: replayFacts } : {}),
|
|
111
|
+
passAtK: { baseline: a.passAtK, candidate: b.passAtK },
|
|
112
|
+
passPowK: { baseline: a.passPowK, candidate: b.passPowK },
|
|
113
|
+
measured,
|
|
114
|
+
...(ctx.sameRates && ctx.tokensComparable && costBefore !== undefined && costAfter !== undefined
|
|
115
|
+
? { estimated: { costUsdPerSuccess: delta(costBefore, costAfter) } }
|
|
116
|
+
: {}),
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
const sameRates = (a, b) => a !== undefined &&
|
|
120
|
+
b !== undefined &&
|
|
121
|
+
a.inputPerMTokUsd === b.inputPerMTokUsd &&
|
|
122
|
+
a.outputPerMTokUsd === b.outputPerMTokUsd;
|
|
123
|
+
/** Compares `candidate` against `baseline`; throws `EvalCompareError` for different suites. */
|
|
124
|
+
export function compareReports(baseline, candidate) {
|
|
125
|
+
if (baseline.suiteId !== candidate.suiteId)
|
|
126
|
+
throw new EvalCompareError(`Cannot compare different suites: baseline is "${baseline.suiteId}", candidate is "${candidate.suiteId}".`, "suite_id");
|
|
127
|
+
if (baseline.suiteVersion !== candidate.suiteVersion)
|
|
128
|
+
throw new EvalCompareError(`Cannot compare different suite versions of "${baseline.suiteId}": baseline is ${baseline.suiteVersion}, candidate is ${candidate.suiteVersion}. A changed fixture or verifier changes what is measured; rerun both on the same version.`, "suite_version");
|
|
129
|
+
const candidateTasks = new Map(candidate.tasks.map((t) => [t.taskId, t]));
|
|
130
|
+
const baselineIds = new Set(baseline.tasks.map((t) => t.taskId));
|
|
131
|
+
const rates = sameRates(baseline.rates, candidate.rates);
|
|
132
|
+
const modeDiffers = baseline.conditions.mode !== candidate.conditions.mode;
|
|
133
|
+
const compactionA = baseline.conditions.compactionHash;
|
|
134
|
+
const compactionB = candidate.conditions.compactionHash;
|
|
135
|
+
const compactionDiffers = compactionA !== undefined && compactionB !== undefined && compactionA !== compactionB;
|
|
136
|
+
const context = {
|
|
137
|
+
sameRates: rates,
|
|
138
|
+
tokensComparable: !compactionDiffers,
|
|
139
|
+
modeDiffers,
|
|
140
|
+
replay: baseline.conditions.mode === "replay" && candidate.conditions.mode === "replay",
|
|
141
|
+
};
|
|
142
|
+
const tasks = baseline.tasks.flatMap((t) => {
|
|
143
|
+
const other = candidateTasks.get(t.taskId);
|
|
144
|
+
return other ? [compareTask(t, other, context)] : [];
|
|
145
|
+
});
|
|
146
|
+
const warnings = [];
|
|
147
|
+
const weak = tasks.filter((t) => t.lowPower).map((t) => t.taskId);
|
|
148
|
+
if (weak.length > 0 && !context.replay)
|
|
149
|
+
warnings.push(`Low power: ${weak.join(", ")} ${weak.length === 1 ? "has" : "have"} fewer than ${MIN_POWER_K} judged runs on at least one side; no improvement or regression is claimed for ${weak.length === 1 ? "it" : "them"}.`);
|
|
150
|
+
const erroredTasks = tasks.filter((t) => t.errored.baseline > 0 || t.errored.candidate > 0);
|
|
151
|
+
if (erroredTasks.length > 0)
|
|
152
|
+
warnings.push(`Errored runs (infrastructure failures excluded from the rates) on ${erroredTasks.map((t) => `${t.taskId} (base ${t.errored.baseline} / cand ${t.errored.candidate})`).join(", ")}: the judged runs are a filtered sample, so no improvement or regression is claimed for ${erroredTasks.length === 1 ? "it" : "them"}. Fix the cause and rerun.`);
|
|
153
|
+
const mismatched = tasks.filter((t) => t.kMismatch).map((t) => t.taskId);
|
|
154
|
+
if (mismatched.length > 0)
|
|
155
|
+
warnings.push(`Different k between the sides on ${mismatched.join(", ")}: pass^k is only comparable at equal k.`);
|
|
156
|
+
if (baseline.rates && candidate.rates && !rates)
|
|
157
|
+
warnings.push("The two reports were estimated with different rates: costs are not compared.");
|
|
158
|
+
if (modeDiffers)
|
|
159
|
+
warnings.push(`The modes differ (${baseline.conditions.mode} vs ${candidate.conditions.mode}): replay measures the harness, not the model, so these are different kinds of measurement; no improvement or regression is claimed.`);
|
|
160
|
+
if (compactionDiffers)
|
|
161
|
+
warnings.push("The compaction settings differ between the reports: token and cost deltas are not computed, because compaction calls are not in the measured main-loop tokens.");
|
|
162
|
+
else if ((compactionA === undefined) !== (compactionB === undefined))
|
|
163
|
+
warnings.push("The compaction settings of one report are unknown (it predates the compaction identity): token and cost deltas assume they are equal.");
|
|
164
|
+
const shared = baseline.tasks
|
|
165
|
+
.map((t) => t.taskId)
|
|
166
|
+
.filter((id) => candidateTasks.has(id))
|
|
167
|
+
.sort();
|
|
168
|
+
const hashA = sharedConfigHash(baseline, shared);
|
|
169
|
+
const hashB = sharedConfigHash(candidate, shared);
|
|
170
|
+
const valueOf = (field, r, shared) => (field === "configHash" ? (shared ?? r.conditions.configHash) : r.conditions[field]);
|
|
171
|
+
return {
|
|
172
|
+
schemaVersion: 1,
|
|
173
|
+
suiteId: baseline.suiteId,
|
|
174
|
+
suiteVersion: baseline.suiteVersion,
|
|
175
|
+
baseline: baseline.conditions,
|
|
176
|
+
candidate: candidate.conditions,
|
|
177
|
+
changed: CHANGED_FIELDS.flatMap((field) => {
|
|
178
|
+
const before = valueOf(field, baseline, hashA);
|
|
179
|
+
const after = valueOf(field, candidate, hashB);
|
|
180
|
+
return before === after ? [] : [{ field, baseline: before, candidate: after }];
|
|
181
|
+
}),
|
|
182
|
+
errored: { baseline: baseline.summary.errored, candidate: candidate.summary.errored },
|
|
183
|
+
tokenComparison: compactionDiffers
|
|
184
|
+
? "not comparable: compaction settings differ"
|
|
185
|
+
: "comparable",
|
|
186
|
+
tasks,
|
|
187
|
+
onlyInBaseline: baseline.tasks.map((t) => t.taskId).filter((id) => !candidateTasks.has(id)),
|
|
188
|
+
onlyInCandidate: candidate.tasks.map((t) => t.taskId).filter((id) => !baselineIds.has(id)),
|
|
189
|
+
warnings,
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
/**
|
|
193
|
+
* The `configHash` of a report restricted to `ids`: the hash every shared task has, or the hash of
|
|
194
|
+
* the sorted pairs (the scheme `buildReport` uses). `undefined` when a task carries no hash, so
|
|
195
|
+
* the caller falls back to the report-level condition. Comparing only the SHARED tasks keeps a
|
|
196
|
+
* subset run (`--tasks`) from looking like a configuration change.
|
|
197
|
+
*/
|
|
198
|
+
function sharedConfigHash(report, ids) {
|
|
199
|
+
if (ids.length === 0)
|
|
200
|
+
return undefined;
|
|
201
|
+
const byId = new Map(report.tasks.map((t) => [t.taskId, t.configHash]));
|
|
202
|
+
const pairs = ids.map((id) => [id, byId.get(id)]);
|
|
203
|
+
if (pairs.some(([, hash]) => hash === undefined))
|
|
204
|
+
return undefined;
|
|
205
|
+
const distinct = new Set(pairs.map(([, hash]) => hash));
|
|
206
|
+
if (distinct.size === 1)
|
|
207
|
+
return pairs[0]?.[1];
|
|
208
|
+
return `sha256:${createHash("sha256").update(JSON.stringify(pairs)).digest("hex")}`;
|
|
209
|
+
}
|
|
210
|
+
const overridesOf = (v) => v.overrides ?? v.report.variant?.overrides ?? {};
|
|
211
|
+
/** Compares every variant after the first against the first (the baseline). */
|
|
212
|
+
export function compareVariants(variants) {
|
|
213
|
+
const [baseline, ...rest] = variants;
|
|
214
|
+
if (!baseline || rest.length === 0)
|
|
215
|
+
throw new EvalCompareError("Comparing variants needs at least two reports.", "suite_id");
|
|
216
|
+
return {
|
|
217
|
+
suiteId: baseline.report.suiteId,
|
|
218
|
+
suiteVersion: baseline.report.suiteVersion,
|
|
219
|
+
baselineId: baseline.id,
|
|
220
|
+
baselineOverrides: overridesOf(baseline),
|
|
221
|
+
variants: rest.map((v) => ({
|
|
222
|
+
id: v.id,
|
|
223
|
+
overrides: overridesOf(v),
|
|
224
|
+
comparison: compareReports(baseline.report, v.report),
|
|
225
|
+
})),
|
|
226
|
+
};
|
|
227
|
+
}
|
|
228
|
+
// ---------------------------------------------------------------------------------------------
|
|
229
|
+
// Report files
|
|
230
|
+
// ---------------------------------------------------------------------------------------------
|
|
231
|
+
const intervalSchema = z.object({
|
|
232
|
+
value: z.number(),
|
|
233
|
+
wilsonLow: z.number(),
|
|
234
|
+
wilsonHigh: z.number(),
|
|
235
|
+
});
|
|
236
|
+
const reportSchema = z.object({
|
|
237
|
+
schemaVersion: z.literal(1, { error: "unsupported schemaVersion (expected 1)" }),
|
|
238
|
+
suiteId: z.string(),
|
|
239
|
+
suiteVersion: z.string(),
|
|
240
|
+
conditions: z.object({
|
|
241
|
+
harnessVersion: z.string(),
|
|
242
|
+
configHash: z.string(),
|
|
243
|
+
provider: z.string(),
|
|
244
|
+
model: z.string(),
|
|
245
|
+
mode: z.enum(["live", "replay"]),
|
|
246
|
+
k: z.number(),
|
|
247
|
+
seedBase: z.number(),
|
|
248
|
+
compactionHash: z.string().optional(),
|
|
249
|
+
}),
|
|
250
|
+
variant: z.object({ id: z.string(), overrides: z.record(z.string(), z.unknown()) }).optional(),
|
|
251
|
+
notes: z.array(z.string()).optional(),
|
|
252
|
+
tasks: z.array(z.object({
|
|
253
|
+
taskId: z.string(),
|
|
254
|
+
k: z.number(),
|
|
255
|
+
successRate: intervalSchema,
|
|
256
|
+
passAtK: z.boolean(),
|
|
257
|
+
passPowK: z.boolean(),
|
|
258
|
+
errored: z.number().optional(),
|
|
259
|
+
configHash: z.string().optional(),
|
|
260
|
+
measured: z.object({
|
|
261
|
+
medianWallMs: z.number().optional(),
|
|
262
|
+
varianceWallMs: z.number().optional(),
|
|
263
|
+
varianceTokens: z.number().optional(),
|
|
264
|
+
tokensPerSuccess: z.number().optional(),
|
|
265
|
+
wallMsPerSuccess: z.number().optional(),
|
|
266
|
+
}),
|
|
267
|
+
estimated: z.object({ costUsdPerSuccess: z.number().optional() }).optional(),
|
|
268
|
+
})),
|
|
269
|
+
runs: z.array(z.string()),
|
|
270
|
+
summary: z.object({
|
|
271
|
+
tasks: z.number(),
|
|
272
|
+
passed: z.number(),
|
|
273
|
+
errored: z.number(),
|
|
274
|
+
budgetExceeded: z.number(),
|
|
275
|
+
runs: z.number().optional(),
|
|
276
|
+
failed: z.number().optional(),
|
|
277
|
+
}),
|
|
278
|
+
rates: z.object({ inputPerMTokUsd: z.number(), outputPerMTokUsd: z.number() }).optional(),
|
|
279
|
+
});
|
|
280
|
+
/** Validates the parsed JSON of a `report.json`; throws an actionable `Error` otherwise. */
|
|
281
|
+
export function parseReport(value) {
|
|
282
|
+
const parsed = reportSchema.safeParse(value);
|
|
283
|
+
if (!parsed.success) {
|
|
284
|
+
const issue = parsed.error.issues[0];
|
|
285
|
+
const where = issue?.path.length ? ` at "${issue.path.join(".")}"` : "";
|
|
286
|
+
throw new Error(`Not a valid reliability report (report.json written by \`alisio eval run\`): ${issue?.message ?? "invalid"}${where}.`);
|
|
287
|
+
}
|
|
288
|
+
return parsed.data;
|
|
289
|
+
}
|
|
290
|
+
// ---------------------------------------------------------------------------------------------
|
|
291
|
+
// Markdown
|
|
292
|
+
// ---------------------------------------------------------------------------------------------
|
|
293
|
+
const cell = (text) => text.replace(/\|/g, "\\|").replace(/\r?\n/g, " ");
|
|
294
|
+
const percent = (value) => `${Math.round(value * 1000) / 10}%`;
|
|
295
|
+
const signed = (value, format) => `${value > 0 ? "+" : value < 0 ? "-" : ""}${format(Math.abs(value))}`;
|
|
296
|
+
const points = (value) => `${Math.round(value * 1000) / 10} pp`;
|
|
297
|
+
const interval = (i, k) => k === 0
|
|
298
|
+
? "n/a"
|
|
299
|
+
: i.wilsonLow === undefined || i.wilsonHigh === undefined
|
|
300
|
+
? percent(i.value)
|
|
301
|
+
: `${percent(i.value)} [${percent(i.wilsonLow)}, ${percent(i.wilsonHigh)}]`;
|
|
302
|
+
const VERDICT_TEXT = {
|
|
303
|
+
improved: "improved",
|
|
304
|
+
regressed: "regressed",
|
|
305
|
+
inconclusive: "inconclusive (intervals overlap)",
|
|
306
|
+
low_power: "low power (k<5, no claim)",
|
|
307
|
+
"not comparable: errored runs": "not comparable: errored runs",
|
|
308
|
+
"not comparable: mode differs": "not comparable: mode differs",
|
|
309
|
+
changed: "changed",
|
|
310
|
+
unchanged: "unchanged",
|
|
311
|
+
};
|
|
312
|
+
const METRIC_LABEL = {
|
|
313
|
+
medianWallMs: "Median wall (ms)",
|
|
314
|
+
varianceWallMs: "Wall variance (ms^2)",
|
|
315
|
+
varianceTokens: "Token variance (main-loop tokens)",
|
|
316
|
+
tokensPerSuccess: "Tokens per success (main-loop tokens, excludes compaction calls)",
|
|
317
|
+
wallMsPerSuccess: "Wall per success (ms)",
|
|
318
|
+
};
|
|
319
|
+
const number = (value) => String(Math.round(value * 1000) / 1000);
|
|
320
|
+
function table(headers, rows) {
|
|
321
|
+
return [
|
|
322
|
+
`| ${headers.join(" | ")} |`,
|
|
323
|
+
`| ${headers.map(() => "---").join(" | ")} |`,
|
|
324
|
+
...rows.map((row) => `| ${row.join(" | ")} |`),
|
|
325
|
+
].join("\n");
|
|
326
|
+
}
|
|
327
|
+
function body(c, level) {
|
|
328
|
+
const lines = [
|
|
329
|
+
`- Baseline: \`${c.baseline.model}\`, mode ${c.baseline.mode}, harness ${c.baseline.harnessVersion}, config \`${c.baseline.configHash}\``,
|
|
330
|
+
`- Candidate: \`${c.candidate.model}\`, mode ${c.candidate.mode}, harness ${c.candidate.harnessVersion}, config \`${c.candidate.configHash}\``,
|
|
331
|
+
"",
|
|
332
|
+
`${level} What changed`,
|
|
333
|
+
"",
|
|
334
|
+
c.changed.length === 0
|
|
335
|
+
? "Nothing among the hashed conditions (configHash, model, harnessVersion, mode, provider)."
|
|
336
|
+
: table(["Condition", "Baseline", "Candidate"], c.changed.map((x) => [x.field, `\`${cell(x.baseline)}\``, `\`${cell(x.candidate)}\``])),
|
|
337
|
+
"",
|
|
338
|
+
"> Built-in plugin options and provider endpoint settings are not hashed: a change in either does not show up here.",
|
|
339
|
+
"",
|
|
340
|
+
];
|
|
341
|
+
if (c.baseline.mode === "replay" || c.candidate.mode === "replay")
|
|
342
|
+
lines.push("> Replay measures the harness, not the model: a replay comparison says nothing about model quality.", "");
|
|
343
|
+
for (const warning of c.warnings)
|
|
344
|
+
lines.push(`> ${warning}`, "");
|
|
345
|
+
if (c.onlyInBaseline.length > 0)
|
|
346
|
+
lines.push(`Only in the baseline (not compared): ${c.onlyInBaseline.join(", ")}`, "");
|
|
347
|
+
if (c.onlyInCandidate.length > 0)
|
|
348
|
+
lines.push(`Only in the candidate (not compared): ${c.onlyInCandidate.join(", ")}`, "");
|
|
349
|
+
const replayOnly = c.baseline.mode === "replay" && c.candidate.mode === "replay";
|
|
350
|
+
lines.push(`${level} Success rate`, "", replayOnly
|
|
351
|
+
? "Replay repeats an identical recording, so no interval is computed: each task is reported as changed or unchanged (pass status and measured counts)."
|
|
352
|
+
: "Wilson 95% intervals. A direction is claimed only with k>=5 on both sides, disjoint intervals and no errored runs; otherwise it stays inconclusive or not comparable.", "", table([
|
|
353
|
+
"Task",
|
|
354
|
+
"k (base / cand)",
|
|
355
|
+
"Errored (base / cand)",
|
|
356
|
+
"Baseline",
|
|
357
|
+
"Candidate",
|
|
358
|
+
"Delta",
|
|
359
|
+
"Verdict",
|
|
360
|
+
], c.tasks.map((t) => [
|
|
361
|
+
cell(t.taskId),
|
|
362
|
+
`${t.k.baseline} / ${t.k.candidate}${t.kMismatch ? " (k differs)" : ""}`,
|
|
363
|
+
`${t.errored.baseline} / ${t.errored.candidate}`,
|
|
364
|
+
interval(t.successRate.baseline, t.k.baseline),
|
|
365
|
+
interval(t.successRate.candidate, t.k.candidate),
|
|
366
|
+
signed(t.successRate.delta, points),
|
|
367
|
+
VERDICT_TEXT[t.successRate.verdict],
|
|
368
|
+
])), "", `${level} Measured`, "", c.tokenComparison === "comparable"
|
|
369
|
+
? "Deltas are candidate minus baseline over provider-reported usage and wall time. Token figures are main-loop tokens (excludes compaction calls). They carry no verdict."
|
|
370
|
+
: "Deltas are candidate minus baseline over wall time. Token deltas are not compared: not comparable: compaction settings differ (compaction calls are not in the main-loop tokens). They carry no verdict.", "");
|
|
371
|
+
const measuredRows = c.tasks.flatMap((t) => MEASURED_KEYS.flatMap((key) => {
|
|
372
|
+
const d = t.measured[key];
|
|
373
|
+
return d
|
|
374
|
+
? [
|
|
375
|
+
[
|
|
376
|
+
cell(t.taskId),
|
|
377
|
+
METRIC_LABEL[key],
|
|
378
|
+
number(d.baseline),
|
|
379
|
+
number(d.candidate),
|
|
380
|
+
signed(d.delta, number),
|
|
381
|
+
],
|
|
382
|
+
]
|
|
383
|
+
: [];
|
|
384
|
+
}));
|
|
385
|
+
lines.push(measuredRows.length === 0
|
|
386
|
+
? "No metric could be computed on both sides."
|
|
387
|
+
: table(["Task", "Metric", "Baseline", "Candidate", "Delta"], measuredRows), "", `${level} Estimated`, "");
|
|
388
|
+
const estimatedRows = c.tasks.flatMap((t) => {
|
|
389
|
+
const d = t.estimated?.costUsdPerSuccess;
|
|
390
|
+
return d
|
|
391
|
+
? [
|
|
392
|
+
[
|
|
393
|
+
cell(t.taskId),
|
|
394
|
+
`$${d.baseline.toFixed(4)}`,
|
|
395
|
+
`$${d.candidate.toFixed(4)}`,
|
|
396
|
+
signed(d.delta, (v) => `$${v.toFixed(4)}`),
|
|
397
|
+
],
|
|
398
|
+
]
|
|
399
|
+
: [];
|
|
400
|
+
});
|
|
401
|
+
lines.push(estimatedRows.length === 0
|
|
402
|
+
? "Not compared: the reports carry no cost estimate, were estimated with different rates, or differ in compaction settings."
|
|
403
|
+
: table(["Task", "Cost per success (base)", "Candidate", "Delta"], estimatedRows));
|
|
404
|
+
return lines;
|
|
405
|
+
}
|
|
406
|
+
/** The footnote every comparison Markdown carries (statistical limits, stated once, verbatim). */
|
|
407
|
+
function footnote(tasks, errored, drift) {
|
|
408
|
+
return `*improved/regressed = 95% Wilson intervals disjoint with at least 5 judged runs per side. Conservative screen, not a significance test; 'inconclusive' is not 'no difference'. Assumes independent repetitions; ${drift}; no correction across ${tasks} tasks; ${errored} runs excluded as infrastructure errors; seeds order runs only and do not reach the provider.*`;
|
|
409
|
+
}
|
|
410
|
+
/** Markdown of a comparison: what changed, verdicts, then Measured and Estimated apart. */
|
|
411
|
+
export function renderComparisonMarkdown(c) {
|
|
412
|
+
const note = footnote(c.tasks.length, c.errored.baseline + c.errored.candidate, "the two reports ran separately, so time drift is not controlled");
|
|
413
|
+
return `${[`# Comparison: ${c.suiteId} ${c.suiteVersion}`, "", ...body(c, "##"), "", note].join("\n")}\n`;
|
|
414
|
+
}
|
|
415
|
+
/** Markdown of a variants comparison: one section per candidate variant against the baseline. */
|
|
416
|
+
export function renderVariantComparisonMarkdown(v) {
|
|
417
|
+
const overrides = (o) => Object.keys(o).length === 0
|
|
418
|
+
? "no overrides"
|
|
419
|
+
: Object.entries(o)
|
|
420
|
+
.map(([key, value]) => `\`${key}\` = \`${JSON.stringify(value)}\``)
|
|
421
|
+
.join(", ");
|
|
422
|
+
const lines = [
|
|
423
|
+
`# Variants: ${v.suiteId} ${v.suiteVersion}`,
|
|
424
|
+
"",
|
|
425
|
+
`Baseline variant: \`${v.baselineId}\` (${overrides(v.baselineOverrides)}). Each section compares one variant against it, over the same tasks; seeds only order the plan, nothing is paired, and the unpaired intervals are independent.`,
|
|
426
|
+
"",
|
|
427
|
+
];
|
|
428
|
+
for (const { id, overrides: own, comparison } of v.variants)
|
|
429
|
+
lines.push(`## ${id} vs ${v.baselineId}`, "", `Overrides of \`${id}\`: ${overrides(own)}.`, "", ...body(comparison, "###"), "");
|
|
430
|
+
const first = v.variants[0]?.comparison;
|
|
431
|
+
const errored = (first?.errored.baseline ?? 0) +
|
|
432
|
+
v.variants.reduce((n, x) => n + x.comparison.errored.candidate, 0);
|
|
433
|
+
const tasks = v.variants.reduce((n, x) => n + x.comparison.tasks.length, 0);
|
|
434
|
+
lines.push(footnote(tasks, errored, "variants ran interleaved but time drift is not fully controlled"));
|
|
435
|
+
return `${lines.join("\n").trimEnd()}\n`;
|
|
436
|
+
}
|