@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.
@@ -2,12 +2,68 @@ import { canonicalFingerprint } from "./artifacts.js";
2
2
  import { evaluateRoute } from "./router.js";
3
3
  import { normalizeUsage } from "./usage.js";
4
4
 
5
- export const BENCHMARK_VERSION = "1";
5
+ export const BENCHMARK_VERSION = "2";
6
+ export const SUPPORTED_BENCHMARK_VERSIONS = Object.freeze(["1", "2"]);
6
7
  export const BENCHMARK_MODES = Object.freeze([
7
8
  "direct",
8
9
  "forgeloopBalanced",
9
10
  "forgeloopAdaptive",
10
11
  ]);
12
+ // Observational runaway-execution diagnostics only; they never gate lifecycle
13
+ // truth or completion validity.
14
+ export const BENCHMARK_RUNAWAY_SIGNALS = Object.freeze([
15
+ "EXCESSIVE_MODEL_TURNS",
16
+ "EXCESSIVE_TOOL_CALLS",
17
+ "EXCESSIVE_VERIFICATION_CYCLES",
18
+ "REPEATED_CONTEXT_REFRESH",
19
+ "REPEATED_FILE_READ",
20
+ "REPEATED_GUIDE_LOAD",
21
+ "UNEXPECTED_RETRY",
22
+ "CORRECTION_LOOP",
23
+ "HOST_RETRY",
24
+ "MODEL_STALL",
25
+ "UNKNOWN_TOKEN_SPIKE",
26
+ ]);
27
+ export const BENCHMARK_DIAGNOSTIC_COUNT_FIELDS = Object.freeze([
28
+ "verificationCycles",
29
+ "modelTurns",
30
+ "toolCalls",
31
+ "retries",
32
+ "correctionCycles",
33
+ "filesRead",
34
+ "filesWritten",
35
+ "contextRefreshes",
36
+ ]);
37
+ export const BENCHMARK_TIERS = Object.freeze({
38
+ smoke: Object.freeze({ minimumRuns: 1, maximumRuns: 3, defaultRuns: 1 }),
39
+ evidence: Object.freeze({ minimumRuns: 5, maximumRuns: 10, defaultRuns: 5 }),
40
+ tail: Object.freeze({ minimumRuns: 20, maximumRuns: 30, defaultRuns: 20 }),
41
+ });
42
+ export const OUTLIER_POLICY = "TOKEN_IQR_1_5";
43
+ export const OUTLIER_MINIMUM_SAMPLES = 4;
44
+ export const TAIL_SAMPLE_MINIMUM = 20;
45
+ export const LOW_BASELINE_RATIO = 0.60;
46
+ export const BASELINE_TOKEN_REGIMES = Object.freeze([
47
+ "NORMAL",
48
+ "LOW_BASELINE_TOKEN_REGIME",
49
+ ]);
50
+ export const TAIL_STATUSES = Object.freeze([
51
+ "TAIL_STABLE",
52
+ "TAIL_WARNING",
53
+ "TAIL_REGRESSION",
54
+ "NOT_ENOUGH_SAMPLES",
55
+ ]);
56
+ export const DISTRIBUTION_TAIL_STATUSES = Object.freeze([
57
+ "TAIL_ACCEPTABLE",
58
+ "TAIL_REGRESSION",
59
+ "NOT_ENOUGH_SAMPLES",
60
+ ]);
61
+ export const COMBINED_TAIL_STATUSES = Object.freeze([
62
+ "TAIL_CONSISTENT",
63
+ "TAIL_PAIRED_RATIO_SENSITIVE",
64
+ "TAIL_DISTRIBUTION_REGRESSION",
65
+ "TAIL_UNRESOLVED",
66
+ ]);
11
67
  export const BENCHMARK_VERIFICATION_RESULTS = Object.freeze([
12
68
  "PASS",
13
69
  "FAIL",
@@ -306,6 +362,81 @@ export function normalizeBenchmarkQuality(value) {
306
362
  return { source, scores: normalizedScores };
307
363
  }
308
364
 
365
+ function assertStringArray(value, label) {
366
+ if (!Array.isArray(value)) throw benchmarkError(`${label} must be an array`);
367
+ for (const item of value) {
368
+ if (typeof item !== "string" || item.length === 0) {
369
+ throw benchmarkError(`${label} must contain only non-empty strings`);
370
+ }
371
+ }
372
+ return value;
373
+ }
374
+
375
+ // Every diagnostics field is nullable. Unavailable host telemetry stays null;
376
+ // it is never estimated from prompt size, file size, or elapsed time.
377
+ export function normalizeBenchmarkDiagnostics(value) {
378
+ if (value === undefined || value === null) return null;
379
+ const diagnostics = assertObject(value, "diagnostics");
380
+ const allowedKeys = new Set([
381
+ "executionProfile",
382
+ ...BENCHMARK_DIAGNOSTIC_COUNT_FIELDS,
383
+ "guideCount",
384
+ "guideIds",
385
+ "contextRefreshes",
386
+ "hostWarnings",
387
+ "terminationReason",
388
+ "flags",
389
+ ]);
390
+ const unknownKey = Object.keys(diagnostics).find((key) => !allowedKeys.has(key));
391
+ if (unknownKey) throw benchmarkError(`diagnostics contains unsupported field: ${unknownKey}`);
392
+ const executionProfile = diagnostics.executionProfile ?? null;
393
+ assertNullableProfile(executionProfile, "diagnostics.executionProfile");
394
+ const normalized = { executionProfile };
395
+ for (const field of BENCHMARK_DIAGNOSTIC_COUNT_FIELDS) {
396
+ normalized[field] = assertNullableNonNegativeInteger(diagnostics[field] ?? null, `diagnostics.${field}`);
397
+ }
398
+ normalized.guideCount = assertNullableNonNegativeInteger(diagnostics.guideCount ?? null, "diagnostics.guideCount");
399
+ normalized.guideIds = assertStringArray(diagnostics.guideIds ?? [], "diagnostics.guideIds");
400
+ normalized.hostWarnings = assertStringArray(diagnostics.hostWarnings ?? [], "diagnostics.hostWarnings");
401
+ normalized.terminationReason = assertString(diagnostics.terminationReason ?? null, "diagnostics.terminationReason", { nullable: true });
402
+ const flags = assertStringArray(diagnostics.flags ?? [], "diagnostics.flags");
403
+ const unknownFlag = flags.find((flag) => !BENCHMARK_RUNAWAY_SIGNALS.includes(flag));
404
+ if (unknownFlag) throw benchmarkError(`diagnostics.flags contains unsupported signal: ${unknownFlag}`);
405
+ normalized.flags = BENCHMARK_RUNAWAY_SIGNALS.filter((signal) => flags.includes(signal));
406
+ return normalized;
407
+ }
408
+
409
+ // Observational runaway-execution signals. Host-reported flags are preserved;
410
+ // deterministic signals are added only where the recorded diagnostics support
411
+ // them. A token outlier with no explanatory signal becomes UNKNOWN_TOKEN_SPIKE.
412
+ export function evaluateRunawaySignals(run, {
413
+ medianModelTurns = null,
414
+ medianToolCalls = null,
415
+ tokenOutlier = false,
416
+ } = {}) {
417
+ const diagnostics = normalizeBenchmarkDiagnostics(run?.diagnostics ?? null);
418
+ const signals = new Set(diagnostics?.flags ?? []);
419
+ const doubled = (value, median) => (
420
+ typeof value === "number"
421
+ && typeof median === "number"
422
+ && median > 0
423
+ && value > median * 2
424
+ );
425
+ if (doubled(diagnostics?.modelTurns ?? null, medianModelTurns)) signals.add("EXCESSIVE_MODEL_TURNS");
426
+ if (doubled(diagnostics?.toolCalls ?? null, medianToolCalls)) signals.add("EXCESSIVE_TOOL_CALLS");
427
+ const verificationCycles = diagnostics?.verificationCycles ?? run?.verificationCycles ?? null;
428
+ if (verificationCycles !== null && verificationCycles > 1) signals.add("EXCESSIVE_VERIFICATION_CYCLES");
429
+ if ((diagnostics?.contextRefreshes ?? null) !== null && diagnostics.contextRefreshes > 1) {
430
+ signals.add("REPEATED_CONTEXT_REFRESH");
431
+ }
432
+ if ((diagnostics?.retries ?? null) !== null && diagnostics.retries > 0) signals.add("UNEXPECTED_RETRY");
433
+ if ((diagnostics?.correctionCycles ?? null) !== null && diagnostics.correctionCycles > 1) {
434
+ signals.add("CORRECTION_LOOP");
435
+ }
436
+ if (tokenOutlier && signals.size === 0) signals.add("UNKNOWN_TOKEN_SPIKE");
437
+ return BENCHMARK_RUNAWAY_SIGNALS.filter((signal) => signals.has(signal));
438
+ }
439
+
309
440
  function contextUsageForRun(run) {
310
441
  return normalizeBenchmarkContextUsage(run.contextUsage, run.metadata?.resolvedProfile ?? null);
311
442
  }
@@ -352,8 +483,8 @@ function assertBenchmarkMetadata(metadata, run) {
352
483
  export function assertBenchmarkRun(value) {
353
484
  const run = assertObject(value, "benchmark run");
354
485
  if (run.schemaVersion !== 1) throw benchmarkError("benchmark run schemaVersion must be 1");
355
- if (run.benchmarkVersion !== BENCHMARK_VERSION) {
356
- throw benchmarkError(`benchmark run benchmarkVersion must be ${BENCHMARK_VERSION}`);
486
+ if (!SUPPORTED_BENCHMARK_VERSIONS.includes(run.benchmarkVersion)) {
487
+ throw benchmarkError(`benchmark run benchmarkVersion must be one of ${SUPPORTED_BENCHMARK_VERSIONS.join(", ")}`);
357
488
  }
358
489
  assertRunSetId(run.runSetId);
359
490
  assertRunId(run.runId);
@@ -369,6 +500,7 @@ export function assertBenchmarkRun(value) {
369
500
  assertNullableNonNegativeInteger(run.verificationCycles, "verificationCycles");
370
501
  assertNullableNonNegativeInteger(run.comparableSteps, "comparableSteps");
371
502
  assertBenchmarkMetadata(run.metadata, run);
503
+ if ("diagnostics" in run) normalizeBenchmarkDiagnostics(run.diagnostics);
372
504
  normalizeBenchmarkContextUsage(run.contextUsage, run.metadata.resolvedProfile);
373
505
  normalizeBenchmarkQuality(run.quality);
374
506
  if (usage.model !== null && run.metadata.model !== usage.model) {
@@ -392,6 +524,7 @@ export function createBenchmarkRun({
392
524
  verification = "NOT_AVAILABLE",
393
525
  verificationCycles = null,
394
526
  comparableSteps = null,
527
+ diagnostics = undefined,
395
528
  contextUsage = undefined,
396
529
  quality = undefined,
397
530
  metadata = {},
@@ -402,6 +535,7 @@ export function createBenchmarkRun({
402
535
  if (!profile) throw benchmarkError(`Unsupported benchmark mode: ${mode}`);
403
536
  const resolvedProfile = metadata.resolvedProfile ?? profile.resolvedProfile;
404
537
  normalizedModeProfile(mode, resolvedProfile);
538
+ const normalizedDiagnostics = normalizeBenchmarkDiagnostics(diagnostics);
405
539
  const run = {
406
540
  schemaVersion: 1,
407
541
  benchmarkVersion: BENCHMARK_VERSION,
@@ -416,6 +550,7 @@ export function createBenchmarkRun({
416
550
  verification,
417
551
  verificationCycles,
418
552
  comparableSteps,
553
+ ...(normalizedDiagnostics === null ? {} : { diagnostics: normalizedDiagnostics }),
419
554
  contextUsage: normalizeBenchmarkContextUsage(contextUsage, resolvedProfile),
420
555
  quality: normalizeBenchmarkQuality(quality),
421
556
  metadata: {
@@ -439,23 +574,70 @@ export function createBenchmarkRun({
439
574
  return assertBenchmarkRun(run);
440
575
  }
441
576
 
577
+ function sortedFinite(values) {
578
+ return values.filter((value) => typeof value === "number" && Number.isFinite(value)).sort((a, b) => a - b);
579
+ }
580
+
581
+ function percentileOf(sorted, fraction) {
582
+ const position = (sorted.length - 1) * fraction;
583
+ const lower = Math.floor(position);
584
+ const upper = Math.ceil(position);
585
+ if (lower === upper) return sorted[lower];
586
+ return sorted[lower] + ((sorted[upper] - sorted[lower]) * (position - lower));
587
+ }
588
+
442
589
  function statistic(values) {
443
- const usable = values.filter((value) => typeof value === "number" && Number.isFinite(value)).sort((a, b) => a - b);
590
+ const usable = sortedFinite(values);
444
591
  if (usable.length === 0) return { count: 0, average: null, p50: null, p95: null, minimum: null, maximum: null };
445
- const percentile = (fraction) => {
446
- const position = (usable.length - 1) * fraction;
447
- const lower = Math.floor(position);
448
- const upper = Math.ceil(position);
449
- if (lower === upper) return usable[lower];
450
- return usable[lower] + ((usable[upper] - usable[lower]) * (position - lower));
592
+ return {
593
+ count: usable.length,
594
+ average: Number((usable.reduce((sum, value) => sum + value, 0) / usable.length).toFixed(4)),
595
+ p50: Number(percentileOf(usable, 0.5).toFixed(4)),
596
+ p95: Number(percentileOf(usable, 0.95).toFixed(4)),
597
+ minimum: usable[0],
598
+ maximum: usable.at(-1),
451
599
  };
600
+ }
601
+
602
+ // Benchmark methodology v2 favors robust statistics for skewed token
603
+ // distributions: median/IQR/MAD instead of average/stddev alone.
604
+ export function robustStatistic(values) {
605
+ const usable = sortedFinite(values);
606
+ if (usable.length === 0) {
607
+ return {
608
+ count: 0,
609
+ average: null,
610
+ minimum: null,
611
+ p25: null,
612
+ p50: null,
613
+ p75: null,
614
+ p90: null,
615
+ p95: null,
616
+ maximum: null,
617
+ iqr: null,
618
+ mad: null,
619
+ outlierCount: 0,
620
+ };
621
+ }
622
+ const q1 = percentileOf(usable, 0.25);
623
+ const median = percentileOf(usable, 0.5);
624
+ const q3 = percentileOf(usable, 0.75);
625
+ const iqr = q3 - q1;
626
+ const upperFence = q3 + 1.5 * iqr;
627
+ const deviations = usable.map((value) => Math.abs(value - median)).sort((a, b) => a - b);
452
628
  return {
453
629
  count: usable.length,
454
630
  average: Number((usable.reduce((sum, value) => sum + value, 0) / usable.length).toFixed(4)),
455
- p50: Number(percentile(0.5).toFixed(4)),
456
- p95: Number(percentile(0.95).toFixed(4)),
457
631
  minimum: usable[0],
632
+ p25: Number(q1.toFixed(4)),
633
+ p50: Number(median.toFixed(4)),
634
+ p75: Number(q3.toFixed(4)),
635
+ p90: Number(percentileOf(usable, 0.9).toFixed(4)),
636
+ p95: Number(percentileOf(usable, 0.95).toFixed(4)),
458
637
  maximum: usable.at(-1),
638
+ iqr: Number(iqr.toFixed(4)),
639
+ mad: Number(percentileOf(deviations, 0.5).toFixed(4)),
640
+ outlierCount: usable.filter((value) => value > upperFence).length,
459
641
  };
460
642
  }
461
643
 
@@ -487,7 +669,18 @@ function comparablePair(left, right) {
487
669
  return fields.every((field) => (left.metadata[field] ?? null) === (right.metadata[field] ?? null));
488
670
  }
489
671
 
490
- function comparisonForMode(directRuns, modeRuns) {
672
+ export function distributionDeltaPercent(baselineStats, candidateStats) {
673
+ if (!baselineStats || !candidateStats) return { p50: null, p95: null };
674
+ const p50Delta = (Number.isFinite(baselineStats.p50) && Number.isFinite(candidateStats.p50) && baselineStats.p50 > 0)
675
+ ? Number((((candidateStats.p50 - baselineStats.p50) / baselineStats.p50) * 100).toFixed(4))
676
+ : null;
677
+ const p95Delta = (Number.isFinite(baselineStats.p95) && Number.isFinite(candidateStats.p95) && baselineStats.p95 > 0)
678
+ ? Number((((candidateStats.p95 - baselineStats.p95) / baselineStats.p95) * 100).toFixed(4))
679
+ : null;
680
+ return { p50: p50Delta, p95: p95Delta };
681
+ }
682
+
683
+ function legacyComparisonForMode(directRuns, modeRuns) {
491
684
  const directByIndex = new Map(directRuns.map((run) => [run.runIndex, run]));
492
685
  const pairs = modeRuns
493
686
  .map((run) => ({ variant: run, direct: directByIndex.get(run.runIndex) }))
@@ -515,28 +708,113 @@ function comparisonForMode(directRuns, modeRuns) {
515
708
  };
516
709
  }
517
710
 
518
- function contextUsageAggregate(runs) {
711
+ function comparisonForMode(directRuns, modeRuns) {
712
+ const directByIndex = new Map(directRuns.map((run) => [run.runIndex, run]));
713
+ const pairs = modeRuns
714
+ .map((run) => ({ variant: run, direct: directByIndex.get(run.runIndex) }))
715
+ .filter((pair) => pair.direct && comparablePair(pair.direct, pair.variant));
716
+
717
+ const validTokenPairs = pairs.filter(({ direct, variant }) => (
718
+ Number.isFinite(direct.usage?.totalTokens)
719
+ && Number.isFinite(variant.usage?.totalTokens)
720
+ && direct.usage.totalTokens > 0
721
+ ));
722
+ const validTimePairs = pairs.filter(({ direct, variant }) => (
723
+ Number.isFinite(direct.wallClockMs)
724
+ && Number.isFinite(variant.wallClockMs)
725
+ && direct.wallClockMs > 0
726
+ ));
727
+
728
+ const comparableDirectTokens = validTokenPairs.map(({ direct }) => direct.usage.totalTokens);
729
+ const comparableCandidateTokens = validTokenPairs.map(({ variant }) => variant.usage.totalTokens);
730
+
731
+ const comparableDirectStats = robustStatistic(comparableDirectTokens);
732
+ const comparableCandidateStats = robustStatistic(comparableCandidateTokens);
733
+
734
+ const distDelta = distributionDeltaPercent(comparableDirectStats, comparableCandidateStats);
735
+
736
+ const lowBaselineThreshold = (Number.isFinite(comparableDirectStats.p50) && comparableDirectStats.p50 > 0)
737
+ ? Number((comparableDirectStats.p50 * LOW_BASELINE_RATIO).toFixed(4))
738
+ : null;
739
+
740
+ const pairedRuns = validTokenPairs.map(({ direct, variant }) => {
741
+ const directTokens = direct.usage.totalTokens;
742
+ const candidateTokens = variant.usage.totalTokens;
743
+ const pairedOverhead = Number((((candidateTokens - directTokens) / directTokens) * 100).toFixed(4));
744
+ const absoluteDelta = candidateTokens - directTokens;
745
+ const baselineRegime = (lowBaselineThreshold !== null && directTokens < lowBaselineThreshold)
746
+ ? "LOW_BASELINE_TOKEN_REGIME"
747
+ : "NORMAL";
748
+ return {
749
+ runIndex: variant.runIndex,
750
+ directTokens,
751
+ candidateTokens,
752
+ absoluteDeltaTokens: absoluteDelta,
753
+ pairedOverheadPercent: pairedOverhead,
754
+ baselineRegime,
755
+ };
756
+ });
757
+
758
+ const tokenOverheads = validTokenPairs.map(({ direct, variant }) => (
759
+ ((variant.usage.totalTokens - direct.usage.totalTokens) / direct.usage.totalTokens) * 100
760
+ ));
761
+ const timeOverheads = validTimePairs.map(({ direct, variant }) => (
762
+ ((variant.wallClockMs - direct.wallClockMs) / direct.wallClockMs) * 100
763
+ ));
764
+
765
+ const tokenStats = statistic(tokenOverheads);
766
+ const timeStats = statistic(timeOverheads);
767
+ const claimAllowed = tokenStats.count > 0 && timeStats.count > 0;
768
+
769
+ const lowBaselinePairCount = pairedRuns.filter((p) => p.baselineRegime === "LOW_BASELINE_TOKEN_REGIME").length;
770
+
771
+ return {
772
+ comparablePairs: pairs.length,
773
+ tokenComparablePairs: tokenStats.count,
774
+ timeComparablePairs: timeStats.count,
775
+ tokenOverheadPercent: { p50: tokenStats.p50, p95: tokenStats.p95 },
776
+ pairedOverheadPercent: { p50: tokenStats.p50, p95: tokenStats.p95 },
777
+ distributionDeltaPercent: distDelta,
778
+ timeOverheadPercent: { p50: timeStats.p50, p95: timeStats.p95 },
779
+ pairedRatioDiagnostics: {
780
+ pairCount: pairs.length,
781
+ baselineMinimum: comparableDirectStats.minimum,
782
+ baselineP25: comparableDirectStats.p25,
783
+ baselineP50: comparableDirectStats.p50,
784
+ lowBaselineThreshold,
785
+ lowBaselinePairCount,
786
+ },
787
+ pairedRuns,
788
+ claimStatus: claimAllowed ? "OBSERVATIONAL" : "NOT_COMPARABLE",
789
+ claimAllowed,
790
+ reason: claimAllowed
791
+ ? "Trusted usage, actual timing, verification, and matching comparability metadata are present."
792
+ : "Efficiency claims require trusted usage, actual timing, PASS verification, positive comparable steps, and matching metadata.",
793
+ };
794
+ }
795
+
796
+ function contextUsageAggregate(runs, statisticFn = statistic) {
519
797
  const contextUsages = runs.map(contextUsageForRun);
520
798
  const totals = contextUsages.map(measuredContextTokens);
521
799
  return {
522
800
  sources: [...new Set(contextUsages.map((contextUsage) => contextUsage.source))].sort(),
523
801
  profiles: [...new Set(contextUsages.map((contextUsage) => contextUsage.profile).filter(Boolean))].sort(),
524
802
  measuredRuns: totals.filter((value) => value !== null).length,
525
- totalTokens: statistic(totals),
803
+ totalTokens: statisticFn(totals),
526
804
  items: Object.fromEntries(BENCHMARK_CONTEXT_USAGE_ITEMS.map((item) => [
527
805
  item,
528
- statistic(contextUsages.map((contextUsage) => contextUsage.items[item])),
806
+ statisticFn(contextUsages.map((contextUsage) => contextUsage.items[item])),
529
807
  ])),
530
808
  };
531
809
  }
532
810
 
533
- function qualityAggregate(runs) {
811
+ function qualityAggregate(runs, statisticFn = statistic) {
534
812
  const quality = runs.map((run) => normalizeBenchmarkQuality(run.quality));
535
813
  return {
536
814
  sources: [...new Set(quality.map((item) => item.source))].sort(),
537
815
  scores: Object.fromEntries(BENCHMARK_QUALITY_FIELDS.map((field) => [
538
816
  field,
539
- statistic(quality.map((item) => item.scores[field])),
817
+ statisticFn(quality.map((item) => item.scores[field])),
540
818
  ])),
541
819
  };
542
820
  }
@@ -578,6 +856,138 @@ function contextInflationForScenario(balancedRuns, adaptiveRuns) {
578
856
  };
579
857
  }
580
858
 
859
+ // Deterministic benchmark-level outlier classification. The IQR rule is an
860
+ // analysis convention, not protocol truth; it never changes lifecycle state.
861
+ export function analyzeTokenOutliers(runsByMode) {
862
+ return {
863
+ policy: OUTLIER_POLICY,
864
+ minimumSamples: OUTLIER_MINIMUM_SAMPLES,
865
+ modes: Object.fromEntries(BENCHMARK_MODES.map((mode) => [
866
+ mode,
867
+ tokenOutlierAnalysisForMode(runsByMode[mode] ?? []),
868
+ ])),
869
+ };
870
+ }
871
+
872
+ function tokenOutlierAnalysisForMode(runs) {
873
+ const measured = runs.filter((run) => Number.isFinite(run.usage.totalTokens));
874
+ const notEnough = {
875
+ sampleCount: runs.length,
876
+ measuredCount: measured.length,
877
+ status: "NOT_ENOUGH_SAMPLES",
878
+ q1: null,
879
+ q3: null,
880
+ iqr: null,
881
+ median: null,
882
+ upperFence: null,
883
+ outliers: [],
884
+ };
885
+ if (measured.length < OUTLIER_MINIMUM_SAMPLES) return notEnough;
886
+ const stats = robustStatistic(measured.map((run) => run.usage.totalTokens));
887
+ const medianModelTurns = robustStatistic(measured.map((run) => run.diagnostics?.modelTurns ?? null)).p50;
888
+ const medianToolCalls = robustStatistic(measured.map((run) => run.diagnostics?.toolCalls ?? null)).p50;
889
+ const upperFence = stats.p75 + 1.5 * stats.iqr;
890
+ const outliers = measured
891
+ .filter((run) => run.usage.totalTokens > upperFence)
892
+ .map((run) => ({
893
+ runId: run.runId,
894
+ runIndex: run.runIndex,
895
+ totalTokens: run.usage.totalTokens,
896
+ scenarioMedianTokens: stats.p50,
897
+ ratioToMedian: stats.p50 > 0 ? Number((run.usage.totalTokens / stats.p50).toFixed(4)) : null,
898
+ reasons: ["TOKEN_IQR_OUTLIER"],
899
+ diagnosticSignals: evaluateRunawaySignals(run, { medianModelTurns, medianToolCalls, tokenOutlier: true }),
900
+ }))
901
+ .sort((left, right) => left.runIndex - right.runIndex);
902
+ return {
903
+ sampleCount: runs.length,
904
+ measuredCount: measured.length,
905
+ status: "MEASURED",
906
+ q1: stats.p25,
907
+ q3: stats.p75,
908
+ iqr: stats.iqr,
909
+ median: stats.p50,
910
+ upperFence: Number(upperFence.toFixed(4)),
911
+ outliers,
912
+ };
913
+ }
914
+
915
+ // Observational tail stability status; the thresholds are benchmark policy,
916
+ // never lifecycle policy.
917
+ export function classifyTailStatus({
918
+ sampleCount,
919
+ p95TokenOverheadPercent,
920
+ outlierCount = 0,
921
+ sampleMinimum = TAIL_SAMPLE_MINIMUM,
922
+ regressionPercent = LIGHT_EFFICIENCY_OBJECTIVES.p95TokenOverheadPercent,
923
+ } = {}) {
924
+ if (!Number.isInteger(sampleCount) || sampleCount < sampleMinimum) return "NOT_ENOUGH_SAMPLES";
925
+ if (p95TokenOverheadPercent === null || p95TokenOverheadPercent === undefined) return "NOT_ENOUGH_SAMPLES";
926
+ if (p95TokenOverheadPercent > regressionPercent) return "TAIL_REGRESSION";
927
+ if (outlierCount > 0) return "TAIL_WARNING";
928
+ return "TAIL_STABLE";
929
+ }
930
+
931
+ export function classifyDistributionTailStatus({
932
+ sampleCount,
933
+ distributionP95DeltaPercent,
934
+ sampleMinimum = TAIL_SAMPLE_MINIMUM,
935
+ regressionPercent = LIGHT_EFFICIENCY_OBJECTIVES.p95TokenOverheadPercent,
936
+ } = {}) {
937
+ if (!Number.isInteger(sampleCount) || sampleCount < sampleMinimum) return "NOT_ENOUGH_SAMPLES";
938
+ if (distributionP95DeltaPercent === null || distributionP95DeltaPercent === undefined) return "NOT_ENOUGH_SAMPLES";
939
+ if (distributionP95DeltaPercent > regressionPercent) return "TAIL_REGRESSION";
940
+ return "TAIL_ACCEPTABLE";
941
+ }
942
+
943
+ export function classifyCombinedTailStatus({
944
+ pairedStatus,
945
+ distributionStatus,
946
+ lowBaselinePairCount = 0,
947
+ sampleCount,
948
+ sampleMinimum = TAIL_SAMPLE_MINIMUM,
949
+ } = {}) {
950
+ if (!Number.isInteger(sampleCount) || sampleCount < sampleMinimum) return "TAIL_UNRESOLVED";
951
+ if (pairedStatus === "NOT_ENOUGH_SAMPLES" || distributionStatus === "NOT_ENOUGH_SAMPLES") return "TAIL_UNRESOLVED";
952
+ if (distributionStatus === "TAIL_REGRESSION") return "TAIL_DISTRIBUTION_REGRESSION";
953
+ if (pairedStatus === "TAIL_REGRESSION" && distributionStatus === "TAIL_ACCEPTABLE" && lowBaselinePairCount > 0) {
954
+ return "TAIL_PAIRED_RATIO_SENSITIVE";
955
+ }
956
+ if (pairedStatus === "TAIL_STABLE" && distributionStatus === "TAIL_ACCEPTABLE") return "TAIL_CONSISTENT";
957
+ if (pairedStatus === "TAIL_WARNING" && distributionStatus === "TAIL_ACCEPTABLE") return "TAIL_CONSISTENT";
958
+ if (pairedStatus === distributionStatus) return "TAIL_CONSISTENT";
959
+ return "TAIL_UNRESOLVED";
960
+ }
961
+
962
+ function diagnosticsAggregate(runs) {
963
+ return Object.fromEntries(BENCHMARK_DIAGNOSTIC_COUNT_FIELDS.map((field) => [
964
+ field,
965
+ robustStatistic(runs.map((run) => run.diagnostics?.[field] ?? null)),
966
+ ]));
967
+ }
968
+
969
+ function modeAggregateRobust(runs, { includeContextUsage = false, includeQuality = false } = {}) {
970
+ const verificationCount = runs.filter((run) => run.verification === "PASS").length;
971
+ const tokenPerStep = runs.map((run) => (
972
+ Number.isFinite(run.usage.totalTokens) && Number.isInteger(run.comparableSteps) && run.comparableSteps > 0
973
+ ? run.usage.totalTokens / run.comparableSteps
974
+ : null
975
+ ));
976
+ return {
977
+ runCount: runs.length,
978
+ verificationSuccessRate: runs.length > 0 ? Number((verificationCount / runs.length).toFixed(4)) : null,
979
+ usageSources: [...new Set(runs.map((run) => run.usage.source))].sort(),
980
+ totalTokens: robustStatistic(runs.map((run) => run.usage.totalTokens)),
981
+ wallClockMs: robustStatistic(runs.map((run) => run.wallClockMs)),
982
+ verificationCycles: robustStatistic(runs.map((run) => run.verificationCycles)),
983
+ comparableSteps: robustStatistic(runs.map((run) => run.comparableSteps)),
984
+ tokensPerComparableStep: robustStatistic(tokenPerStep),
985
+ diagnostics: diagnosticsAggregate(runs),
986
+ ...(includeContextUsage ? { contextUsage: contextUsageAggregate(runs, robustStatistic) } : {}),
987
+ ...(includeQuality ? { quality: qualityAggregate(runs, robustStatistic) } : {}),
988
+ };
989
+ }
990
+
581
991
  function modeAggregate(runs, { includeContextUsage = false, includeQuality = false } = {}) {
582
992
  const verificationCount = runs.filter((run) => run.verification === "PASS").length;
583
993
  const comparableSteps = runs.map((run) => run.comparableSteps);
@@ -600,6 +1010,21 @@ function modeAggregate(runs, { includeContextUsage = false, includeQuality = fal
600
1010
  };
601
1011
  }
602
1012
 
1013
+ function lightObjectivesFor(scenario, comparisons) {
1014
+ if (scenario.expectedProfile !== "light") return null;
1015
+ return {
1016
+ p50TokenOverheadPercent: LIGHT_EFFICIENCY_OBJECTIVES.p50TokenOverheadPercent,
1017
+ p95TokenOverheadPercent: LIGHT_EFFICIENCY_OBJECTIVES.p95TokenOverheadPercent,
1018
+ status: comparisons.forgeloopAdaptive?.claimAllowed ? "OBSERVATIONAL" : "NOT_VERIFIED",
1019
+ p50Pass: comparisons.forgeloopAdaptive?.tokenOverheadPercent.p50 === null
1020
+ ? null
1021
+ : comparisons.forgeloopAdaptive.tokenOverheadPercent.p50 <= LIGHT_EFFICIENCY_OBJECTIVES.p50TokenOverheadPercent,
1022
+ p95Pass: comparisons.forgeloopAdaptive?.tokenOverheadPercent.p95 === null
1023
+ ? null
1024
+ : comparisons.forgeloopAdaptive.tokenOverheadPercent.p95 <= LIGHT_EFFICIENCY_OBJECTIVES.p95TokenOverheadPercent,
1025
+ };
1026
+ }
1027
+
603
1028
  export function aggregateBenchmarkRuns({ scenario, runs } = {}) {
604
1029
  assertBenchmarkScenario(scenario);
605
1030
  if (!Array.isArray(runs) || runs.length === 0) throw benchmarkError("runs must contain at least one benchmark run");
@@ -620,33 +1045,81 @@ export function aggregateBenchmarkRuns({ scenario, runs } = {}) {
620
1045
  duplicateKeys.add(key);
621
1046
  }
622
1047
  const directRuns = byMode.direct;
1048
+ const includeContextUsage = runs.some((run) => Object.prototype.hasOwnProperty.call(run, "contextUsage"));
1049
+ const includeQuality = runs.some((run) => Object.prototype.hasOwnProperty.call(run, "quality"));
1050
+ const legacy = runs.every((run) => run.benchmarkVersion === "1");
1051
+ if (legacy) {
1052
+ const comparisons = Object.fromEntries(BENCHMARK_MODES.map((mode) => [
1053
+ mode,
1054
+ mode === "direct" ? null : legacyComparisonForMode(directRuns, byMode[mode]),
1055
+ ]));
1056
+ const lightObjectives = lightObjectivesFor(scenario, comparisons);
1057
+ return {
1058
+ schemaVersion: 1,
1059
+ benchmarkVersion: "1",
1060
+ runSetId: runs[0].runSetId,
1061
+ scenarioId: scenario.scenarioId,
1062
+ expectedProfile: scenario.expectedProfile,
1063
+ modeAggregates: Object.fromEntries(BENCHMARK_MODES.map((mode) => [mode, modeAggregate(byMode[mode], { includeContextUsage, includeQuality })])),
1064
+ comparisons,
1065
+ lightObjectives,
1066
+ sourcePolicy: "PROVIDER_REPORTED_OR_HOST_REPORTED_ONLY",
1067
+ claimsAllowed: Object.values(comparisons).some((comparison) => comparison?.claimAllowed === true),
1068
+ generatedFromRunCount: runs.length,
1069
+ ...(includeContextUsage
1070
+ ? { contextInflation: contextInflationForScenario(byMode.forgeloopBalanced, byMode.forgeloopAdaptive) }
1071
+ : {}),
1072
+ };
1073
+ }
623
1074
  const comparisons = Object.fromEntries(BENCHMARK_MODES.map((mode) => [
624
1075
  mode,
625
1076
  mode === "direct" ? null : comparisonForMode(directRuns, byMode[mode]),
626
1077
  ]));
627
- const lightObjectives = scenario.expectedProfile === "light"
628
- ? {
629
- p50TokenOverheadPercent: LIGHT_EFFICIENCY_OBJECTIVES.p50TokenOverheadPercent,
630
- p95TokenOverheadPercent: LIGHT_EFFICIENCY_OBJECTIVES.p95TokenOverheadPercent,
631
- status: comparisons.forgeloopAdaptive?.claimAllowed ? "OBSERVATIONAL" : "NOT_VERIFIED",
632
- p50Pass: comparisons.forgeloopAdaptive?.tokenOverheadPercent.p50 === null
633
- ? null
634
- : comparisons.forgeloopAdaptive.tokenOverheadPercent.p50 <= LIGHT_EFFICIENCY_OBJECTIVES.p50TokenOverheadPercent,
635
- p95Pass: comparisons.forgeloopAdaptive?.tokenOverheadPercent.p95 === null
636
- ? null
637
- : comparisons.forgeloopAdaptive.tokenOverheadPercent.p95 <= LIGHT_EFFICIENCY_OBJECTIVES.p95TokenOverheadPercent,
638
- }
639
- : null;
640
- const includeContextUsage = runs.some((run) => Object.prototype.hasOwnProperty.call(run, "contextUsage"));
641
- const includeQuality = runs.some((run) => Object.prototype.hasOwnProperty.call(run, "quality"));
1078
+ const lightObjectives = lightObjectivesFor(scenario, comparisons);
1079
+ const outlierAnalysis = analyzeTokenOutliers(byMode);
1080
+ const comparisonsWithTail = Object.fromEntries(BENCHMARK_MODES.map((mode) => {
1081
+ const comparison = comparisons[mode];
1082
+ if (comparison === null) return [mode, null];
1083
+ const outlierCount = outlierAnalysis.modes[mode].outliers.length;
1084
+ const pairedStatus = classifyTailStatus({
1085
+ sampleCount: comparison.tokenComparablePairs,
1086
+ p95TokenOverheadPercent: comparison.tokenOverheadPercent.p95,
1087
+ outlierCount,
1088
+ });
1089
+ const distributionStatus = classifyDistributionTailStatus({
1090
+ sampleCount: comparison.tokenComparablePairs,
1091
+ distributionP95DeltaPercent: comparison.distributionDeltaPercent.p95,
1092
+ });
1093
+ const combinedInterpretation = classifyCombinedTailStatus({
1094
+ pairedStatus,
1095
+ distributionStatus,
1096
+ lowBaselinePairCount: comparison.pairedRatioDiagnostics?.lowBaselinePairCount ?? 0,
1097
+ sampleCount: comparison.tokenComparablePairs,
1098
+ });
1099
+ return [mode, {
1100
+ ...comparison,
1101
+ tail: {
1102
+ sampleMinimum: TAIL_SAMPLE_MINIMUM,
1103
+ sampleCount: comparison.tokenComparablePairs,
1104
+ p95TokenOverheadPercent: comparison.tokenOverheadPercent.p95,
1105
+ pairedOverheadP95Percent: comparison.pairedOverheadPercent.p95,
1106
+ distributionP95DeltaPercent: comparison.distributionDeltaPercent.p95,
1107
+ outlierCount,
1108
+ status: pairedStatus,
1109
+ pairedStatus,
1110
+ distributionStatus,
1111
+ combinedInterpretation,
1112
+ },
1113
+ }];
1114
+ }));
642
1115
  return {
643
1116
  schemaVersion: 1,
644
1117
  benchmarkVersion: BENCHMARK_VERSION,
645
1118
  runSetId: runs[0].runSetId,
646
1119
  scenarioId: scenario.scenarioId,
647
1120
  expectedProfile: scenario.expectedProfile,
648
- modeAggregates: Object.fromEntries(BENCHMARK_MODES.map((mode) => [mode, modeAggregate(byMode[mode], { includeContextUsage, includeQuality })])),
649
- comparisons,
1121
+ modeAggregates: Object.fromEntries(BENCHMARK_MODES.map((mode) => [mode, modeAggregateRobust(byMode[mode], { includeContextUsage, includeQuality })])),
1122
+ comparisons: comparisonsWithTail,
650
1123
  lightObjectives,
651
1124
  sourcePolicy: "PROVIDER_REPORTED_OR_HOST_REPORTED_ONLY",
652
1125
  claimsAllowed: Object.values(comparisons).some((comparison) => comparison?.claimAllowed === true),
@@ -654,6 +1127,7 @@ export function aggregateBenchmarkRuns({ scenario, runs } = {}) {
654
1127
  ...(includeContextUsage
655
1128
  ? { contextInflation: contextInflationForScenario(byMode.forgeloopBalanced, byMode.forgeloopAdaptive) }
656
1129
  : {}),
1130
+ outlierAnalysis,
657
1131
  };
658
1132
  }
659
1133