@cassiomc1/forgeloop 1.7.0 → 1.8.1

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.
@@ -0,0 +1,136 @@
1
+ #!/usr/bin/env node
2
+
3
+ import path from "node:path";
4
+ import { fileURLToPath } from "node:url";
5
+
6
+ import {
7
+ analyzeTokenOutliers,
8
+ BENCHMARK_MODES,
9
+ BENCHMARK_VERSION,
10
+ } from "../src/core/execution-profile-benchmarks.js";
11
+ import { readBenchmarkRunSets, readBenchmarkScenarios } from "./lib/execution-profile-benchmark-io.mjs";
12
+
13
+ const repositoryRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
14
+ const defaultResultsDirectory = path.join(repositoryRoot, "benchmarks", "execution-profiles", "results");
15
+
16
+ function parseArgs(argv) {
17
+ const options = { results: defaultResultsDirectory, runSetId: null, json: false };
18
+ for (let index = 0; index < argv.length; index += 1) {
19
+ const argument = argv[index];
20
+ if (argument === "--json") options.json = true;
21
+ else if (["--results", "--run-set"].includes(argument)) {
22
+ const value = argv[++index];
23
+ if (!value || value.startsWith("--")) throw new Error(`${argument} requires a value`);
24
+ if (argument === "--results") options.results = value;
25
+ else options.runSetId = value;
26
+ } else if (argument === "--help" || argument === "-h") options.help = true;
27
+ else throw new Error(`unknown option: ${argument}`);
28
+ }
29
+ return options;
30
+ }
31
+
32
+ function helpText() {
33
+ return "Usage: npm run benchmark:profiles:outliers -- [--results <directory>] [--run-set <id>] [--json]";
34
+ }
35
+
36
+ function diagnosticsValue(run, field) {
37
+ return run?.diagnostics?.[field] ?? null;
38
+ }
39
+
40
+ function buildRows(runSet, scenario) {
41
+ const runs = runSet.runs.filter((run) => run.scenarioId === scenario.scenarioId);
42
+ if (runs.length === 0) return [];
43
+ const byMode = Object.fromEntries(BENCHMARK_MODES.map((mode) => [
44
+ mode,
45
+ runs.filter((run) => run.mode === mode).sort((left, right) => left.runIndex - right.runIndex),
46
+ ]));
47
+ const runById = new Map(runs.map((run) => [run.runId, run]));
48
+ const analysis = analyzeTokenOutliers(byMode);
49
+ const rows = [];
50
+ for (const mode of BENCHMARK_MODES) {
51
+ const modeAnalysis = analysis.modes[mode];
52
+ for (const outlier of modeAnalysis.outliers) {
53
+ const run = runById.get(outlier.runId);
54
+ rows.push({
55
+ scenarioId: scenario.scenarioId,
56
+ mode,
57
+ runId: outlier.runId,
58
+ totalTokens: outlier.totalTokens,
59
+ scenarioMedianTokens: outlier.scenarioMedianTokens,
60
+ ratioToMedian: outlier.ratioToMedian,
61
+ reasons: outlier.reasons,
62
+ diagnosticSignals: outlier.diagnosticSignals,
63
+ verificationCycles: run?.verificationCycles ?? diagnosticsValue(run, "verificationCycles"),
64
+ modelTurns: diagnosticsValue(run, "modelTurns"),
65
+ toolCalls: diagnosticsValue(run, "toolCalls"),
66
+ retries: diagnosticsValue(run, "retries"),
67
+ correctionCycles: diagnosticsValue(run, "correctionCycles"),
68
+ });
69
+ }
70
+ }
71
+ return rows;
72
+ }
73
+
74
+ function formatValue(value) {
75
+ return value === null || value === undefined ? "-" : String(value);
76
+ }
77
+
78
+ async function main() {
79
+ const options = parseArgs(process.argv.slice(2));
80
+ if (options.help) {
81
+ console.log(helpText());
82
+ return;
83
+ }
84
+ const scenarios = await readBenchmarkScenarios(repositoryRoot);
85
+ const runSets = await readBenchmarkRunSets(path.resolve(options.results), options.runSetId);
86
+ const reports = [];
87
+ for (const runSet of runSets) {
88
+ for (const scenario of scenarios) {
89
+ const rows = buildRows(runSet, scenario);
90
+ if (rows.length > 0) reports.push({ runSetId: runSet.runSetId, scenarioId: scenario.scenarioId, outliers: rows });
91
+ }
92
+ }
93
+ const outlierCount = reports.reduce((sum, report) => sum + report.outliers.length, 0);
94
+ const output = {
95
+ schemaVersion: 1,
96
+ benchmarkVersion: BENCHMARK_VERSION,
97
+ policy: "TOKEN_IQR_1_5",
98
+ runSetCount: runSets.length,
99
+ outlierCount,
100
+ reports,
101
+ note: "Outlier classification is a benchmark diagnostic. It never changes lifecycle truth or completion validity.",
102
+ };
103
+ if (options.json) {
104
+ console.log(JSON.stringify(output));
105
+ return;
106
+ }
107
+ if (outlierCount === 0) {
108
+ console.log("No token outliers detected under the TOKEN_IQR_1_5 policy.");
109
+ return;
110
+ }
111
+ const header = ["Scenario", "Mode", "Run ID", "Tokens", "Median", "Ratio", "VerCycles", "Turns", "ToolCalls", "Retries", "Corrections", "Signals"];
112
+ console.log(header.join("\t"));
113
+ for (const report of reports) {
114
+ for (const row of report.outliers) {
115
+ console.log([
116
+ row.scenarioId,
117
+ row.mode,
118
+ row.runId,
119
+ formatValue(row.totalTokens),
120
+ formatValue(row.scenarioMedianTokens),
121
+ formatValue(row.ratioToMedian),
122
+ formatValue(row.verificationCycles),
123
+ formatValue(row.modelTurns),
124
+ formatValue(row.toolCalls),
125
+ formatValue(row.retries),
126
+ formatValue(row.correctionCycles),
127
+ row.diagnosticSignals.length > 0 ? row.diagnosticSignals.join(",") : "-",
128
+ ].join("\t"));
129
+ }
130
+ }
131
+ }
132
+
133
+ main().catch((error) => {
134
+ console.error(`ForgeLoop benchmark outlier report: ${error.message}`);
135
+ process.exitCode = 1;
136
+ });
@@ -0,0 +1,156 @@
1
+ #!/usr/bin/env node
2
+
3
+ import path from "node:path";
4
+ import { fileURLToPath } from "node:url";
5
+
6
+ import {
7
+ aggregateBenchmarkRuns,
8
+ BENCHMARK_VERSION,
9
+ } from "../src/core/execution-profile-benchmarks.js";
10
+ import { readBenchmarkRunSets, readBenchmarkScenarios } from "./lib/execution-profile-benchmark-io.mjs";
11
+
12
+ const repositoryRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
13
+ const defaultResultsDirectory = path.join(repositoryRoot, "benchmarks", "execution-profiles", "results");
14
+
15
+ function parseArgs(argv) {
16
+ const options = { results: defaultResultsDirectory, runSetId: null, json: false };
17
+ for (let index = 0; index < argv.length; index += 1) {
18
+ const argument = argv[index];
19
+ if (argument === "--json") options.json = true;
20
+ else if (["--results", "--run-set"].includes(argument)) {
21
+ const value = argv[++index];
22
+ if (!value || value.startsWith("--")) throw new Error(`${argument} requires a value`);
23
+ if (argument === "--results") options.results = value;
24
+ else options.runSetId = value;
25
+ } else if (argument === "--help" || argument === "-h") options.help = true;
26
+ else throw new Error(`unknown option: ${argument}`);
27
+ }
28
+ return options;
29
+ }
30
+
31
+ function helpText() {
32
+ return "Usage: npm run benchmark:profiles:tail-analysis -- [--results <directory>] [--run-set <id>] [--json]";
33
+ }
34
+
35
+ function buildScenarioReport(aggregate) {
36
+ const directAgg = aggregate.modeAggregates?.direct;
37
+ const adaptiveComp = aggregate.comparisons?.forgeloopAdaptive;
38
+ const adaptiveAgg = aggregate.modeAggregates?.forgeloopAdaptive;
39
+
40
+ const directTokens = directAgg?.totalTokens ?? {};
41
+ const adaptiveTokens = adaptiveAgg?.totalTokens ?? {};
42
+
43
+ const paired = adaptiveComp?.pairedOverheadPercent ?? {};
44
+ const dist = adaptiveComp?.distributionDeltaPercent ?? {};
45
+ const tail = adaptiveComp?.tail ?? {};
46
+ const diag = adaptiveComp?.pairedRatioDiagnostics ?? {};
47
+
48
+ const hasComparableRuns = (adaptiveComp?.pairedRuns?.length ?? 0) > 0;
49
+ let compDirectP50 = diag.baselineP50 ?? directTokens.p50 ?? null;
50
+ let compDirectP95 = directTokens.p95 ?? null;
51
+ let compAdaptiveP50 = adaptiveTokens.p50 ?? null;
52
+ let compAdaptiveP95 = adaptiveTokens.p95 ?? null;
53
+
54
+ if (hasComparableRuns) {
55
+ const dTokens = adaptiveComp.pairedRuns.map((r) => r.directTokens).sort((a, b) => a - b);
56
+ const aTokens = adaptiveComp.pairedRuns.map((r) => r.candidateTokens).sort((a, b) => a - b);
57
+ const pos95 = (dTokens.length - 1) * 0.95;
58
+ const lower = Math.floor(pos95);
59
+ const upper = Math.ceil(pos95);
60
+ const calcP95 = (arr) => (lower === upper ? arr[lower] : arr[lower] + ((arr[upper] - arr[lower]) * (pos95 - lower)));
61
+ const pos50 = (dTokens.length - 1) * 0.5;
62
+ const lower50 = Math.floor(pos50);
63
+ const upper50 = Math.ceil(pos50);
64
+ const calcP50 = (arr) => (lower50 === upper50 ? arr[lower50] : arr[lower50] + ((arr[upper50] - arr[lower50]) * (pos50 - lower50)));
65
+
66
+ compDirectP50 = Number(calcP50(dTokens).toFixed(4));
67
+ compDirectP95 = Number(calcP95(dTokens).toFixed(4));
68
+ compAdaptiveP50 = Number(calcP50(aTokens).toFixed(4));
69
+ compAdaptiveP95 = Number(calcP95(aTokens).toFixed(4));
70
+ }
71
+
72
+ return {
73
+ scenarioId: aggregate.scenarioId,
74
+ expectedProfile: aggregate.expectedProfile,
75
+ totalRunsPerMode: directAgg?.runCount ?? 0,
76
+ verificationSuccessRate: adaptiveAgg?.verificationSuccessRate ?? 1,
77
+ directP50: compDirectP50,
78
+ adaptiveP50: compAdaptiveP50,
79
+ distributionP50DeltaPercent: dist.p50 ?? null,
80
+ directP95: compDirectP95,
81
+ adaptiveP95: compAdaptiveP95,
82
+ distributionP95DeltaPercent: dist.p95 ?? null,
83
+ pairedOverheadP50Percent: paired.p50 ?? null,
84
+ pairedOverheadP95Percent: paired.p95 ?? null,
85
+ lowBaselinePairCount: diag.lowBaselinePairCount ?? 0,
86
+ comparablePairCount: adaptiveComp?.tokenComparablePairs ?? 0,
87
+ pairedStatus: tail.pairedStatus ?? "NOT_ENOUGH_SAMPLES",
88
+ distributionStatus: tail.distributionStatus ?? "NOT_ENOUGH_SAMPLES",
89
+ combinedInterpretation: tail.combinedInterpretation ?? "TAIL_UNRESOLVED",
90
+ pairedRuns: adaptiveComp?.pairedRuns ?? [],
91
+ };
92
+ }
93
+
94
+ async function main() {
95
+ const options = parseArgs(process.argv.slice(2));
96
+ if (options.help) {
97
+ console.log(helpText());
98
+ return;
99
+ }
100
+ const scenarios = await readBenchmarkScenarios(repositoryRoot);
101
+ const runSets = await readBenchmarkRunSets(path.resolve(options.results), options.runSetId);
102
+ const reports = [];
103
+
104
+ for (const runSet of runSets) {
105
+ const scenarioReports = [];
106
+ for (const scenario of scenarios) {
107
+ const runs = runSet.runs.filter((run) => run.scenarioId === scenario.scenarioId);
108
+ if (runs.length === 0) continue;
109
+ const aggregate = aggregateBenchmarkRuns({ scenario, runs });
110
+ scenarioReports.push(buildScenarioReport(aggregate));
111
+ }
112
+ reports.push({
113
+ runSetId: runSet.runSetId,
114
+ runCount: runSet.runs.length,
115
+ scenarios: scenarioReports,
116
+ });
117
+ }
118
+
119
+ const output = {
120
+ schemaVersion: 1,
121
+ benchmarkVersion: BENCHMARK_VERSION,
122
+ status: reports.length === 0 ? "NOT_MEASURED" : "MEASURED",
123
+ runSets: reports,
124
+ };
125
+
126
+ if (options.json) {
127
+ console.log(JSON.stringify(output, null, 2));
128
+ } else {
129
+ for (const runSet of reports) {
130
+ console.log(`\n======================================================`);
131
+ console.log(`Run Set: ${runSet.runSetId} (${runSet.runCount} runs)`);
132
+ console.log(`======================================================`);
133
+ for (const sc of runSet.scenarios) {
134
+ console.log(`\nScenario: ${sc.scenarioId} (expectedProfile: ${sc.expectedProfile})`);
135
+ console.log(` Direct P50: ${sc.directP50 ?? "N/A"}`);
136
+ console.log(` Adaptive P50: ${sc.adaptiveP50 ?? "N/A"}`);
137
+ console.log(` Distribution P50 Delta: ${sc.distributionP50DeltaPercent !== null ? `${sc.distributionP50DeltaPercent > 0 ? "+" : ""}${sc.distributionP50DeltaPercent}%` : "N/A"}`);
138
+ console.log(` Direct P95: ${sc.directP95 ?? "N/A"}`);
139
+ console.log(` Adaptive P95: ${sc.adaptiveP95 ?? "N/A"}`);
140
+ console.log(` Distribution P95 Delta: ${sc.distributionP95DeltaPercent !== null ? `${sc.distributionP95DeltaPercent > 0 ? "+" : ""}${sc.distributionP95DeltaPercent}%` : "N/A"}`);
141
+ console.log(` Paired Overhead P50: ${sc.pairedOverheadP50Percent !== null ? `${sc.pairedOverheadP50Percent > 0 ? "+" : ""}${sc.pairedOverheadP50Percent}%` : "N/A"}`);
142
+ console.log(` Paired Overhead P95: ${sc.pairedOverheadP95Percent !== null ? `${sc.pairedOverheadP95Percent > 0 ? "+" : ""}${sc.pairedOverheadP95Percent}%` : "N/A"}`);
143
+ console.log(` Low-Baseline Pairs: ${sc.lowBaselinePairCount}`);
144
+ console.log(` Comparable Pairs: ${sc.comparablePairCount}`);
145
+ console.log(` Paired Tail Status: ${sc.pairedStatus}`);
146
+ console.log(` Distribution Tail Status: ${sc.distributionStatus}`);
147
+ console.log(` Combined Interpretation: ${sc.combinedInterpretation}`);
148
+ }
149
+ }
150
+ }
151
+ }
152
+
153
+ main().catch((error) => {
154
+ console.error(`ForgeLoop tail interpretation report: ${error.message}`);
155
+ process.exitCode = 1;
156
+ });
@@ -12,6 +12,7 @@ import { assertSchema, readSchema } from "../src/core/schema-validation.js";
12
12
  import {
13
13
  aggregateBenchmarkRuns,
14
14
  BENCHMARK_MODES,
15
+ BENCHMARK_TIERS,
15
16
  BENCHMARK_VERSION,
16
17
  assertBenchmarkScenario,
17
18
  assertRequiredBenchmarkScenarios,
@@ -32,7 +33,8 @@ function parseArgs(argv) {
32
33
  const options = {
33
34
  target: process.cwd(),
34
35
  adapter: null,
35
- runs: 5,
36
+ runs: null,
37
+ tier: null,
36
38
  runSetId: null,
37
39
  output: defaultOutputDirectory,
38
40
  json: false,
@@ -47,6 +49,7 @@ function parseArgs(argv) {
47
49
  if (argument === "--target") options.target = value();
48
50
  else if (argument === "--adapter") options.adapter = value();
49
51
  else if (argument === "--runs") options.runs = Number(value());
52
+ else if (argument === "--tier") options.tier = value();
50
53
  else if (argument === "--run-set") options.runSetId = value();
51
54
  else if (argument === "--output") options.output = value();
52
55
  else if (argument === "--json") options.json = true;
@@ -56,9 +59,19 @@ function parseArgs(argv) {
56
59
  }
57
60
  if (options.help) return options;
58
61
  if (!options.adapter) throw usageError("--adapter is required; ForgeLoop never invents provider or host measurements");
62
+ if (options.tier !== null && !Object.prototype.hasOwnProperty.call(BENCHMARK_TIERS, options.tier)) {
63
+ throw usageError(`--tier must be one of ${Object.keys(BENCHMARK_TIERS).join(", ")}`);
64
+ }
65
+ const tier = options.tier === null ? null : BENCHMARK_TIERS[options.tier];
66
+ if (options.runs === null) {
67
+ options.runs = tier === null ? 5 : tier.defaultRuns;
68
+ }
59
69
  if (!Number.isInteger(options.runs) || options.runs < 1 || options.runs > 100) {
60
70
  throw usageError("--runs must be an integer from 1 through 100");
61
71
  }
72
+ if (tier !== null && (options.runs < tier.minimumRuns || options.runs > tier.maximumRuns)) {
73
+ throw usageError(`--runs must be between ${tier.minimumRuns} and ${tier.maximumRuns} for the ${options.tier} tier`);
74
+ }
62
75
  if (options.runSetId !== null && !/^[A-Za-z0-9][A-Za-z0-9_-]{0,63}$/u.test(options.runSetId)) {
63
76
  throw usageError("--run-set must contain only portable identifier characters");
64
77
  }
@@ -70,7 +83,9 @@ function helpText() {
70
83
  "Usage: npm run benchmark:profiles -- --adapter <module> [options]",
71
84
  "",
72
85
  "The adapter must execute each scenario and return actual provider/host usage, verification, comparable steps, and optional host-reported contextUsage.",
73
- "Options: --target <path> --runs <1..100> --run-set <id> --output <directory> --json",
86
+ "An optional finalizeBenchmark hook may attach independently evaluated quality by runner-owned runId after all host timings finish.",
87
+ "Tiers are benchmark sample-size policy: smoke (1-3 runs), evidence (5-10 runs), tail (20-30 runs).",
88
+ "Options: --target <path> --runs <1..100> --tier <smoke|evidence|tail> --run-set <id> --output <directory> --json",
74
89
  ].join("\n");
75
90
  }
76
91
 
@@ -93,11 +108,18 @@ async function loadScenarios() {
93
108
  async function loadAdapter(adapterSpecifier) {
94
109
  const adapterPath = path.resolve(process.cwd(), adapterSpecifier);
95
110
  const adapterModule = await import(pathToFileURL(adapterPath).href);
96
- const adapter = adapterModule.runBenchmark ?? adapterModule.default?.runBenchmark ?? adapterModule.default;
97
- if (typeof adapter !== "function") {
111
+ const defaultExport = adapterModule.default;
112
+ const runBenchmark = adapterModule.runBenchmark
113
+ ?? defaultExport?.runBenchmark
114
+ ?? (typeof defaultExport === "function" ? defaultExport : null);
115
+ if (typeof runBenchmark !== "function") {
98
116
  throw usageError("benchmark adapter must export runBenchmark(input) or be a function default export");
99
117
  }
100
- return adapter;
118
+ return {
119
+ runBenchmark,
120
+ finalizeBenchmark: adapterModule.finalizeBenchmark ?? defaultExport?.finalizeBenchmark ?? null,
121
+ cleanup: adapterModule.cleanup ?? defaultExport?.cleanup ?? null,
122
+ };
101
123
  }
102
124
 
103
125
  function generatedRunSetId() {
@@ -139,14 +161,37 @@ function normalizedAdapterMetadata(response, target, repository, scenario, mode)
139
161
  };
140
162
  }
141
163
 
164
+ function normalizeFinalizationResult(value, records) {
165
+ if (value === undefined || value === null) {
166
+ return { qualityByRunId: {}, summary: { status: "NOT_CONFIGURED" } };
167
+ }
168
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
169
+ throw usageError("benchmark finalizer must return an object");
170
+ }
171
+ const qualityByRunId = value.qualityByRunId ?? {};
172
+ if (!qualityByRunId || typeof qualityByRunId !== "object" || Array.isArray(qualityByRunId)) {
173
+ throw usageError("benchmark finalizer qualityByRunId must be an object");
174
+ }
175
+ const runIds = new Set(records.map((record) => record.runId));
176
+ for (const runId of Object.keys(qualityByRunId)) {
177
+ if (!runIds.has(runId)) throw usageError(`benchmark finalizer returned an unknown runId: ${runId}`);
178
+ }
179
+ return {
180
+ qualityByRunId,
181
+ summary: value.summary && typeof value.summary === "object" && !Array.isArray(value.summary)
182
+ ? value.summary
183
+ : { status: "MEASURED" },
184
+ };
185
+ }
186
+
142
187
  async function executeRuns({ adapter, scenarios, target, runSetId, runs }) {
143
188
  const repository = await currentRepositoryFingerprint(target);
144
- const allRuns = [];
189
+ const records = [];
145
190
  for (const scenario of scenarios) {
146
191
  for (const mode of BENCHMARK_MODES) {
147
192
  for (let runIndex = 1; runIndex <= runs; runIndex += 1) {
148
193
  const started = performance.now();
149
- const response = await adapter({
194
+ const response = await adapter.runBenchmark({
150
195
  benchmarkVersion: BENCHMARK_VERSION,
151
196
  scenario: structuredClone(scenario),
152
197
  mode,
@@ -159,29 +204,50 @@ async function executeRuns({ adapter, scenarios, target, runSetId, runs }) {
159
204
  if (!response || typeof response !== "object" || Array.isArray(response)) {
160
205
  throw usageError(`${scenario.scenarioId}/${mode}/${runIndex}: adapter must return an object`);
161
206
  }
162
- const run = createBenchmarkRun({
207
+ records.push({
163
208
  runSetId,
164
209
  runId: `run-${scenario.scenarioId}-${mode}-${String(runIndex).padStart(3, "0")}`,
165
210
  runIndex,
166
211
  scenario,
167
212
  mode,
168
- usage: response.usage ?? {},
213
+ response,
169
214
  wallClockMs,
170
- verification: response.verification ?? "NOT_AVAILABLE",
171
- verificationCycles: response.verificationCycles ?? null,
172
- comparableSteps: response.comparableSteps ?? null,
173
- contextUsage: response.contextUsage,
174
- quality: response.quality,
175
- metadata: normalizedAdapterMetadata(response, target, repository, scenario, mode),
176
215
  });
177
- allRuns.push(run);
178
216
  }
179
217
  }
180
218
  }
181
- return allRuns;
219
+ const finalization = typeof adapter.finalizeBenchmark === "function"
220
+ ? normalizeFinalizationResult(await adapter.finalizeBenchmark({
221
+ benchmarkVersion: BENCHMARK_VERSION,
222
+ records,
223
+ scenarios,
224
+ target,
225
+ runSetId,
226
+ }), records)
227
+ : normalizeFinalizationResult(null, records);
228
+ const allRuns = records.map((record) => {
229
+ const { scenario, mode, response } = record;
230
+ return createBenchmarkRun({
231
+ runSetId: record.runSetId,
232
+ runId: record.runId,
233
+ runIndex: record.runIndex,
234
+ scenario,
235
+ mode,
236
+ usage: response.usage ?? {},
237
+ wallClockMs: record.wallClockMs,
238
+ verification: response.verification ?? "NOT_AVAILABLE",
239
+ verificationCycles: response.verificationCycles ?? null,
240
+ comparableSteps: response.comparableSteps ?? null,
241
+ diagnostics: response.diagnostics,
242
+ contextUsage: response.contextUsage,
243
+ quality: finalization.qualityByRunId[record.runId] ?? response.quality,
244
+ metadata: normalizedAdapterMetadata(response, target, repository, scenario, mode),
245
+ });
246
+ });
247
+ return { runs: allRuns, finalization: finalization.summary };
182
248
  }
183
249
 
184
- async function writeResults({ outputDirectory, runSetId, scenarios, runs }) {
250
+ async function writeResults({ outputDirectory, runSetId, scenarios, runs, tier }) {
185
251
  const rawDirectory = path.join(outputDirectory, "raw", runSetId);
186
252
  const aggregateDirectory = path.join(outputDirectory, "aggregate", runSetId);
187
253
  try {
@@ -214,6 +280,7 @@ async function writeResults({ outputDirectory, runSetId, scenarios, runs }) {
214
280
  schemaVersion: 1,
215
281
  benchmarkVersion: BENCHMARK_VERSION,
216
282
  runSetId,
283
+ tier,
217
284
  scenarioCount: scenarios.length,
218
285
  runCount: runs.length,
219
286
  claimsAllowed: aggregates.some((aggregate) => aggregate.claimsAllowed),
@@ -223,6 +290,7 @@ async function writeResults({ outputDirectory, runSetId, scenarios, runs }) {
223
290
  claimsAllowed: aggregate.claimsAllowed,
224
291
  lightObjectives: aggregate.lightObjectives,
225
292
  contextInflation: aggregate.contextInflation ?? null,
293
+ outlierAnalysis: aggregate.outlierAnalysis ?? null,
226
294
  })),
227
295
  };
228
296
  await writeFile(path.join(aggregateDirectory, "summary.json"), `${JSON.stringify(summary, null, 2)}\n`, "utf8");
@@ -239,24 +307,32 @@ async function main() {
239
307
  const outputDirectory = path.resolve(options.output);
240
308
  const runSetId = options.runSetId ?? generatedRunSetId();
241
309
  const adapter = await loadAdapter(options.adapter);
242
- const scenarios = await loadScenarios();
243
- const runs = await executeRuns({ adapter, scenarios, target, runSetId, runs: options.runs });
244
- const result = await writeResults({ outputDirectory, runSetId, scenarios, runs });
245
- const output = {
246
- status: "MEASURED",
247
- runSetId,
248
- scenarioCount: scenarios.length,
249
- runCount: runs.length,
250
- outputDirectory,
251
- claimsAllowed: result.summary.claimsAllowed,
252
- };
253
- console.log(options.json ? JSON.stringify(output) : [
254
- `Benchmark run set: ${runSetId}`,
255
- `Scenarios: ${scenarios.length}`,
256
- `Runs: ${runs.length}`,
257
- `Claims allowed: ${result.summary.claimsAllowed ? "yes (observational only)" : "no"}`,
258
- `Results: ${outputDirectory}`,
259
- ].join("\n"));
310
+ try {
311
+ const scenarios = await loadScenarios();
312
+ const execution = await executeRuns({ adapter, scenarios, target, runSetId, runs: options.runs });
313
+ const result = await writeResults({ outputDirectory, runSetId, scenarios, runs: execution.runs, tier: options.tier });
314
+ const output = {
315
+ status: "MEASURED",
316
+ runSetId,
317
+ tier: options.tier,
318
+ scenarioCount: scenarios.length,
319
+ runCount: execution.runs.length,
320
+ outputDirectory,
321
+ claimsAllowed: result.summary.claimsAllowed,
322
+ qualityFinalization: execution.finalization,
323
+ };
324
+ console.log(options.json ? JSON.stringify(output) : [
325
+ `Benchmark run set: ${runSetId}`,
326
+ `Tier: ${options.tier ?? "custom"}`,
327
+ `Scenarios: ${scenarios.length}`,
328
+ `Runs: ${execution.runs.length}`,
329
+ `Claims allowed: ${result.summary.claimsAllowed ? "yes (observational only)" : "no"}`,
330
+ `Quality finalization: ${execution.finalization.status ?? "MEASURED"}`,
331
+ `Results: ${outputDirectory}`,
332
+ ].join("\n"));
333
+ } finally {
334
+ if (typeof adapter.cleanup === "function") await adapter.cleanup();
335
+ }
260
336
  }
261
337
 
262
338
  main().catch((error) => {