Skip to content

experiment(eval): statistical significance layer for non-deterministi…#39

Open
AshwinUgale wants to merge 1 commit into
fullsend-ai:mainfrom
AshwinUgale:experiment/eval-statistical-significance
Open

experiment(eval): statistical significance layer for non-deterministi…#39
AshwinUgale wants to merge 1 commit into
fullsend-ai:mainfrom
AshwinUgale:experiment/eval-statistical-significance

Conversation

@AshwinUgale

Copy link
Copy Markdown

Implements the experiment proposed in fullsend-ai/fullsend#2460.

Why

Agent evals are non-deterministic — the same agent on the same input can produce different outputs, and therefore different scores, from run to run. A single run is a noisy sample, not a fact, which breaks the usual pass/fail assertion model. testing-agents.md flags this as open question #1: "What statistical threshold, and how many runs, make a non-deterministic test reliable?"

Two prior experiments here hit the same wall: promptfoo-eval needed "a custom threshold wrapper" it didn't build, and code-agent-evaluation drew statistical conclusions ("within noise," "statistically equivalent") without a power analysis. This adds the missing machinery — gate noisy runs pass/fail with a stated confidence, and compute how many runs a claim actually needs.

What's here

Everything under eval-statistical-significance/, standard library only (no install step):

File Purpose
significance.py Wilson/bootstrap confidence intervals, a lower-bound threshold_test gate, compare_means (baseline vs. changed), and min_trials_* sample-size calculators.
threshold_check.py CLI that gates promptfoo results JSON on a statistical threshold, exit 0/1 — the wrapper promptfoo-eval flagged as missing.
test_significance.py 24 unit tests, including the four the triage bot requested on the issue.
EXPERIMENT.md / RECOMMENDATION.md Method, findings, and recommended defaults.

Verify in ~30s:

cd eval-statistical-significance
python -m unittest -v
python threshold_check.py fixtures/promptfoo_sample.json --target 0.90   # FAIL, exit 1
python threshold_check.py fixtures/promptfoo_sample.json --target 0.70   # PASS, exit 0

Findings

  • Small-n point estimates don't support the claims made on them. 19/20 looks like 95%, but its 95% CI runs [76%, 99%] — it can't certify a 90% floor. 190/200 (same rate, 10× the trials) tightens to [91%, 97%] and does. Gate on the lower bound, not the point estimate.

  • Reliability costs runs, and now there's a number for it. Reliably catching a 10-point drop — say a pass rate slipping from 95% to 85% — takes about 140 runs per variant. Catching sub-0.1-point differences in a 1–5 judge score takes hundreds to thousands. The doc lists "cost budget for agent testing" as an open worry; this replaces the worry with an actual figure.

  • A quick check against an existing experiment. code-agent-evaluation concluded that two agent variants (V8 vs. V5/V7) were "statistically equivalent" — but it based that on a ~0.04-point difference on a 1–5 scale, measured from only 3 runs each. With that few runs, random run-to-run noise is far bigger than a 0.04 gap, so the data can't actually tell whether the variants differ. "We found no difference" isn't the same as "there is no difference" — and this is exactly the kind of overclaim the significance layer is built to catch. (I'm keeping this loose rather than exact: their raw scores aren't published, and their test design is a bit more efficient than my calculator assumes, so my figures are an upper bound.)

Scope and limits

  • This is step 1, the significance layer. A mutation-testing harness is the documented follow-on and is out of scope here.
  • compare_means isn't finished — it's an early version of the "did the eval catch the mutation?" decision. Two things still need fixing: it currently flags a score change in either direction, but a mutation "kill" should only count a score drop; and when it reports "no difference" from few runs, that usually means we didn't run enough, not that the eval truly missed the mutation. Getting this right is part of the mutation follow-on.
  • Deliberately standalone. Standard-library-only and framework-agnostic by design — it gates any eval framework's output (promptfoo, Inspect, raw JSON) straight from CI with zero install, and adds a mutation kill/survive decision (compare_means) that general-purpose stats libraries don't include.

Cross-references fullsend-ai/fullsend#2460.

…c evals

Signed-off-by: AshwinUgale <ugaleashwin@gmail.com>
@AshwinUgale AshwinUgale requested a review from a team as a code owner July 10, 2026 21:32
@qodo-code-review

Copy link
Copy Markdown

PR Summary by Qodo

experiment(eval): add statistical significance gating for non-deterministic evals

✨ Enhancement 🧪 Tests 📝 Documentation 🕐 40+ Minutes

Grey Divider

AI Description

• Add stdlib-only stats utilities (Wilson/bootstrapped CIs) and sample-size calculators.
• Provide a CLI gate for promptfoo JSON that fails CI below a target lower-bound.
• Document methodology/defaults and add fixtures + unit tests for reproducible verification.
Diagram

graph TD
PF{{"promptfoo"}} --> JSON["results.json"] --> CLI(["threshold_check.py"]) --> SIG["significance.py"]
SIG --> OUT["exit code + printed verdict"]
UT["test_significance.py"] --> SIG
UT --> CLI
DOC["EXPERIMENT/RECOMMENDATION.md"] --> SIG
subgraph Legend
  direction LR
  _ext{{"External tool"}} ~~~ _file["Code/Data file"] ~~~ _cli(["CLI entrypoint"])
end
Loading
High-Level Assessment

The following are alternative approaches to this PR:

1. Use scipy/statsmodels (or deepeval) instead of custom math
  • ➕ More battle-tested implementations of CIs, power analysis, and hypothesis tests
  • ➕ Easier to extend to paired tests, equivalence tests (TOST), and multiple-comparison corrections
  • ➖ Adds dependencies and install/packaging overhead for CI jobs
  • ➖ Harder to keep this experiment framework-agnostic and drop-in
2. Implement paired-by-scenario mean comparison (future-proof for mutation testing)
  • ➕ Better matches common eval designs (same scenario set across variants)
  • ➕ Often requires fewer trials than unpaired calculations (more power)
  • ➖ Needs scenario-level grouping in inputs and more complex data contracts
  • ➖ Out of scope for a v1 “significance layer” focused on simple gating
3. Add explicit 'inconclusive/underpowered' verdicts for compare_means
  • ➕ Avoids the common 'no significance == no difference' misinterpretation
  • ➕ More directly usable as a mutation kill/survive decision primitive
  • ➖ Requires choosing/reporting power assumptions (or adaptive sampling)
  • ➖ May complicate the initial CLI/story if introduced too early

Recommendation: The current stdlib-only, framework-agnostic approach is well-aligned with the stated goal: a zero-install CI gate for noisy evals plus planning calculators. Keep this as v1, but prioritize (in follow-on work) a one-sided, potentially paired, and explicitly 'inconclusive' mean-comparison API before relying on compare_means for mutation testing outcomes.

Files changed (6) +924 / -0

Enhancement (2) +444 / -0
significance.pyAdd stdlib statistical utilities: Wilson/bootstrap CIs, gates, and power sizing +332/-0

Add stdlib statistical utilities: Wilson/bootstrap CIs, gates, and power sizing

• Implements Wilson score confidence intervals and a conservative threshold gate based on the lower bound for binary pass rates. Adds percentile bootstrap confidence intervals and a bootstrap difference-of-means comparator, plus sample-size calculators for proportion and mean effects (alpha/power driven).

eval-statistical-significance/significance.py

threshold_check.pyAdd promptfoo JSON threshold-gating CLI for CI +112/-0

Add promptfoo JSON threshold-gating CLI for CI

• Adds a CLI tool that reads promptfoo results JSON, extracts successes/total via either a stats block or result rows, and gates using the lower Wilson bound against a configured target. Emits a human-readable verdict and returns exit code 0/1/2 for pass/fail/error.

eval-statistical-significance/threshold_check.py

Tests (2) +237 / -0
promptfoo_sample.jsonAdd promptfoo results fixture (19/20) for CLI and tests +31/-0

Add promptfoo results fixture (19/20) for CLI and tests

• Introduces a representative promptfoo results JSON sample used to verify parsing and threshold gating behavior. Encodes both aggregate stats and per-case result rows.

eval-statistical-significance/fixtures/promptfoo_sample.json

test_significance.pyAdd unit tests covering quantiles, CI coverage, power math, and CLI gating +206/-0

Add unit tests covering quantiles, CI coverage, power math, and CLI gating

• Adds 24 tests validating inverse-normal quantiles, Wilson interval values/coverage, bootstrap CI nominal coverage, and sample-size calculations. Includes end-to-end checks for promptfoo JSON parsing and CLI exit codes against expected threshold decisions.

eval-statistical-significance/test_significance.py

Documentation (2) +243 / -0
EXPERIMENT.mdDocument experiment goals, method, results, and follow-on work +99/-0

Document experiment goals, method, results, and follow-on work

• Adds a complete experiment writeup describing the motivation (non-deterministic eval noise), the implemented statistical primitives, and how to reproduce results. Calls out limitations (unknown σ, no multiple-comparison handling) and frames mutation testing as the next step.

eval-statistical-significance/EXPERIMENT.md

RECOMMENDATION.mdPublish recommended defaults and power tables for CI thresholds +144/-0

Publish recommended defaults and power tables for CI thresholds

• Provides concrete guidance for binary pass-rate floors and judge-score deltas, including trial-count tables derived from the new calculators. Clarifies common interpretation pitfalls (non-significant ≠ equivalent) and positions the significance layer as prerequisite for mutation testing.

eval-statistical-significance/RECOMMENDATION.md

@qodo-code-review

Copy link
Copy Markdown

Code Review by Qodo

🐞 Bugs (3) 📘 Rule violations (4) 📜 Skill insights (0)

Context used
✅ Compliance rules (platform): 19 rules
✅ Skills: writing-how-to

Grey Divider


Action required

1. eval-statistical-significance missing 4-digit prefix 📘 Rule violation ⚙ Maintainability
Description
The new experiment directory name does not follow the required NNNN-short-description/ convention
used by this repo. This breaks experiment artifact naming consistency and related tooling/lint
expectations.
Code

eval-statistical-significance/EXPERIMENT.md[R1-4]

+# Experiment: Statistical significance for non-deterministic agent evals
+
+**Status:** Complete (v1)
+**Relates to:** [fullsend-ai/fullsend#2460](https://github.com/fullsend-ai/fullsend/issues/2460) · `testing-agents.md` open question #1 · `experiments/promptfoo-eval` · `experiments/code-agent-evaluation`
Relevance

⭐⭐⭐ High

PR #35 standardized/enforced NNNN-short-description experiment naming via lint-experiment-numbers.

PR-#35

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
Rule 1741093 requires experiment artifacts to be named with a zero-padded 4-digit prefix and
kebab-case description. The newly added experiment lives under eval-statistical-significance/,
which lacks the required prefix; the repo README also documents the expected naming convention.

Rule 1741093: Name experiment artifacts with zero-padded numeric prefix and short description
eval-statistical-significance/EXPERIMENT.md[1-4]
README.md[33-39]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The experiment was added under `eval-statistical-significance/`, which does not match the required experiment artifact naming convention `NNNN-short-description/`.

## Issue Context
The repository’s experiment conventions (and lint tooling) expect a zero-padded 4-digit numeric prefix for each experiment directory.

## Fix Focus Areas
- eval-statistical-significance/EXPERIMENT.md[1-81]
- README.md[33-41]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


2. EXPERIMENT.md missing YAML frontmatter 📘 Rule violation § Compliance
Description
The experiment’s primary markdown document does not start with YAML frontmatter containing at least
title and status. This violates the experiment documentation metadata requirement.
Code

eval-statistical-significance/EXPERIMENT.md[R1-5]

+# Experiment: Statistical significance for non-deterministic agent evals
+
+**Status:** Complete (v1)
+**Relates to:** [fullsend-ai/fullsend#2460](https://github.com/fullsend-ai/fullsend/issues/2460) · `testing-agents.md` open question #1 · `experiments/promptfoo-eval` · `experiments/code-agent-evaluation`
+
Relevance

⭐⭐⭐ High

PR #35 required YAML frontmatter and added lint-experiment-frontmatter; missing frontmatter violates
enforced convention.

PR-#35
PR-#36

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
Rule 1741360 requires experiment markdown documentation files to begin with a YAML frontmatter block
containing title and status. The new EXPERIMENT.md starts with # Experiment: ... and has no
frontmatter block.

Rule 1741360: Experiment markdown files must contain minimal YAML frontmatter
eval-statistical-significance/EXPERIMENT.md[1-5]
README.md[37-39]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`eval-statistical-significance/EXPERIMENT.md` begins directly with an H1 and does not include the required YAML frontmatter block with non-empty `title:` and `status:` fields.

## Issue Context
Existing experiments in this repo use YAML frontmatter at the top of the main experiment markdown (see README conventions).

## Fix Focus Areas
- eval-statistical-significance/EXPERIMENT.md[1-10]
- README.md[37-39]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


3. README missing experiment row 📘 Rule violation ≡ Correctness
Description
A new experiment artifact was added but the root README experiment index table was not updated to
include it. This leaves the index out of sync with experiments on disk.
Code

eval-statistical-significance/EXPERIMENT.md[R1-4]

+# Experiment: Statistical significance for non-deterministic agent evals
+
+**Status:** Complete (v1)
+**Relates to:** [fullsend-ai/fullsend#2460](https://github.com/fullsend-ai/fullsend/issues/2460) · `testing-agents.md` open question #1 · `experiments/promptfoo-eval` · `experiments/code-agent-evaluation`
Relevance

⭐⭐⭐ High

PR #35 added README experiment index + lint-experiment-index to keep table in sync.

PR-#35

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
Rule 1742046 requires the README experiment index to match experiments on disk. The PR adds a new
experiment directory (eval-statistical-significance/), but the README table currently lists
experiments only up through 0023 and does not include any entry for this new experiment.

Rule 1742046: Keep README experiment index in sync with experiments on disk
eval-statistical-significance/EXPERIMENT.md[1-4]
README.md[5-32]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The root `README.md` experiment index table does not include an entry for the newly added experiment.

## Issue Context
The README is the canonical index of experiments and is expected to have exactly one row per experiment artifact present on disk.

## Fix Focus Areas
- README.md[5-32]
- eval-statistical-significance/EXPERIMENT.md[1-5]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Remediation recommended

4. build_parser single-use helper 📘 Rule violation ⚙ Maintainability
Description
The new build_parser() abstraction is introduced but is only used once by main() and does not
replace duplicated logic. This adds an extra abstraction layer without clear reuse benefit.
Code

eval-statistical-significance/threshold_check.py[R81-96]

+def build_parser() -> argparse.ArgumentParser:
+    parser = argparse.ArgumentParser(description=__doc__.splitlines()[0])
+    parser.add_argument("results", help="path to a promptfoo results JSON file")
+    parser.add_argument(
+        "--target", type=float, required=True,
+        help="minimum acceptable pass rate, e.g. 0.90",
+    )
+    parser.add_argument(
+        "--confidence", type=float, default=0.95,
+        help="confidence level for the interval (default 0.95)",
+    )
+    return parser
+
+
+def main(argv: list[str] | None = None) -> int:
+    args = build_parser().parse_args(argv)
Relevance

⭐⭐ Medium

No historical evidence found that repo rejects single-use helpers like build_parser().

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
Rule 1742757 requires new abstractions to be reused (or to replace duplication). In
threshold_check.py, build_parser() is defined and then immediately used only once by main().

Rule 1742757: Avoid introducing unused abstractions in code changes
eval-statistical-significance/threshold_check.py[81-96]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`build_parser()` is a single-use helper that currently only serves one call site and doesn’t reduce duplication.

## Issue Context
The compliance rule aims to avoid introducing new abstractions that aren’t reused or simplifying existing repetition.

## Fix Focus Areas
- eval-statistical-significance/threshold_check.py[81-96]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


5. Hardcoded 95% CI label 🐞 Bug ◔ Observability
Description
ThresholdResult.__str__ always prints “95% CI” even when threshold_test() is called with a different
confidence (e.g. via threshold_check.py --confidence), making CLI/log output misleading.
Code

eval-statistical-significance/significance.py[R130-136]

+    def __str__(self) -> str:  # pragma: no cover - display only
+        verdict = "PASS" if self.passed else "FAIL"
+        return (
+            f"{verdict}: {self.observed_rate:.1%} observed over {self.n} trials "
+            f"(95% CI [{self.lower_bound:.1%}, {self.upper_bound:.1%}], "
+            f"target {self.target_rate:.1%})"
+        )
Relevance

⭐⭐ Medium

No historical evidence found about making CI label reflect configurable confidence in string output.

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The CLI exposes --confidence, and the computation uses that value, but the formatted output still
claims a 95% interval.

eval-statistical-significance/significance.py[139-164]
eval-statistical-significance/significance.py[130-136]
eval-statistical-significance/threshold_check.py[88-108]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
`ThresholdResult.__str__` hardcodes the string `95% CI` even though the interval is computed using the `confidence` parameter. This makes `threshold_check.py` output misleading whenever `--confidence` is not 0.95.

### Issue Context
The CLI supports `--confidence`, and `threshold_test(..., confidence=...)` uses that value to compute the Wilson interval; only the display string is wrong.

### Fix Focus Areas
- eval-statistical-significance/significance.py[119-136]
- eval-statistical-significance/threshold_check.py[88-108]

### Suggested fix
- Add a `confidence: float` field to `ThresholdResult` and populate it in `threshold_test`.
- Update `__str__` to print `{confidence:.0%} CI` (or `{confidence*100:.1f}%`) instead of hardcoding 95%.
- (Optional consistency) Apply the same approach to `ComparisonResult.__str__` as well.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


6. Bootstrap params not validated 🐞 Bug ☼ Reliability
Description
bootstrap_ci() and compare_means() don’t validate confidence, and compare_means() also doesn’t
validate iterations, so invalid inputs can silently produce nonsensical/inverted intervals (e.g.,
confidence>1 yields negative indices) or crash (iterations<=0 yields IndexError from empty diffs).
Code

eval-statistical-significance/significance.py[R171-201]

+def bootstrap_ci(
+    samples: Sequence[float],
+    confidence: float = 0.95,
+    iterations: int = 10_000,
+    statistic: Callable[[Sequence[float]], float] = _mean,
+    seed: int | None = None,
+) -> tuple[float, float]:
+    """Percentile bootstrap confidence interval for an arbitrary statistic.
+
+    Makes no normality assumption, which matters for bounded judge scales
+    (1-5) where the sampling distribution is skewed near the ceiling.
+
+    `seed` is required for reproducible CI runs; leave it None for analysis.
+    """
+    if len(samples) < 2:
+        raise ValueError("need at least 2 samples to bootstrap")
+    if iterations < 1:
+        raise ValueError(f"iterations must be positive, got {iterations!r}")
+
+    rng = random.Random(seed)
+    n = len(samples)
+    resampled = [
+        statistic([samples[rng.randrange(n)] for _ in range(n)])
+        for _ in range(iterations)
+    ]
+    resampled.sort()
+
+    alpha = 1.0 - confidence
+    lo_idx = int(math.floor((alpha / 2.0) * iterations))
+    hi_idx = min(int(math.ceil((1.0 - alpha / 2.0) * iterations)) - 1, iterations - 1)
+    return resampled[lo_idx], resampled[hi_idx]
Relevance

⭐⭐ Medium

No prior review history found on validating confidence/iterations inputs in stats helpers.

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
Both functions compute percentile indices from confidence without checking bounds; compare_means
also loops over iterations without guarding against 0/negative, then indexes the (possibly empty)
list.

eval-statistical-significance/significance.py[171-202]
eval-statistical-significance/significance.py[224-271]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
`bootstrap_ci()` and `compare_means()` lack input validation for `confidence` (and `compare_means()` also lacks validation for `iterations`). Because percentile indices are derived directly from these parameters, out-of-range values can:
- silently compute incorrect bounds (e.g., negative indices when `confidence > 1.0`), or
- crash at runtime (e.g., `iterations <= 0` makes `diffs` empty in `compare_means`, then indexing raises `IndexError`).

### Issue Context
Other helpers already validate similar parameters (e.g., `_z_two_sided` validates confidence), so these APIs should be consistent and fail fast with `ValueError`.

### Fix Focus Areas
- eval-statistical-significance/significance.py[171-202]
- eval-statistical-significance/significance.py[224-271]

### Suggested fix
- In both `bootstrap_ci` and `compare_means`, add `if not 0.0 < confidence < 1.0: raise ValueError(...)`.
- In `compare_means`, add `if iterations < 1: raise ValueError(...)`.
- (Optional) Add unit tests asserting `ValueError` for confidence outside (0,1) and iterations<=0 in `compare_means`.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


View more (1)
7. Truthiness miscounts successes 🐞 Bug ≡ Correctness
Description
extract_counts() increments successes based on Python truthiness for row['success']/row['pass'] and
coerces stats counts via int(), so malformed JSON types (e.g., "false" as a string, or non-integer
counts) can be misinterpreted and produce an incorrect CI gate verdict.
Code

eval-statistical-significance/threshold_check.py[R47-76]

+    if stats is not None and "successes" in stats:
+        successes = int(stats["successes"])
+        failures = int(stats.get("failures", 0))
+        total = successes + failures
+        if total <= 0:
+            raise ValueError("stats block reports zero trials")
+        return successes, total
+
+    # Shape 2: a per-result array with `success` booleans.
+    rows = None
+    if isinstance(root, dict) and isinstance(root.get("results"), list):
+        rows = root["results"]
+    elif isinstance(root, list):
+        rows = root
+    if rows is not None:
+        total = len(rows)
+        if total == 0:
+            raise ValueError("results array is empty")
+        successes = 0
+        for row in rows:
+            if isinstance(row, dict):
+                if "success" in row:
+                    successes += 1 if row["success"] else 0
+                elif "pass" in row:
+                    successes += 1 if row["pass"] else 0
+                else:
+                    raise ValueError("result row has no 'success'/'pass' field")
+            else:
+                raise ValueError("result row is not an object")
+        return successes, total
Relevance

⭐⭐ Medium

No historical evidence found on strict JSON type validation vs truthiness/int() coercion in parsers.

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The per-row parser uses if row['success'] / if row['pass'] which follows truthiness, and the
stats parser uses int(...) which can silently coerce/truncate unexpected types.

eval-statistical-significance/threshold_check.py[47-53]
eval-statistical-significance/threshold_check.py[66-76]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
`extract_counts()` currently treats any truthy value as a success (e.g., the string `'false'` is truthy and would be counted as success) and silently truncates non-integer stats values via `int(...)`. This can lead to incorrect success/total counts and therefore incorrect threshold gating.

### Issue Context
This tool is intended to be a CI gate. When the input JSON is malformed or has unexpected types, it’s safer to fail closed (raise `ValueError`) than to guess.

### Fix Focus Areas
- eval-statistical-significance/threshold_check.py[41-76]

### Suggested fix
- For stats shape:
 - Require `type(stats['successes']) is int` and `type(stats.get('failures',0)) is int` (reject floats/strings/bools).
 - Validate non-negative counts and `total > 0`.
- For row shape:
 - Require `type(row['success']) is bool` / `type(row['pass']) is bool`.
 - Raise `ValueError` if types are not booleans.
- Add a small unit test showing that `'false'` (string) is rejected rather than counted as success.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

Qodo Logo

Comment on lines +1 to +4
# Experiment: Statistical significance for non-deterministic agent evals

**Status:** Complete (v1)
**Relates to:** [fullsend-ai/fullsend#2460](https://github.com/fullsend-ai/fullsend/issues/2460) · `testing-agents.md` open question #1 · `experiments/promptfoo-eval` · `experiments/code-agent-evaluation`

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Action required

1. eval-statistical-significance missing 4-digit prefix 📘 Rule violation ⚙ Maintainability

The new experiment directory name does not follow the required NNNN-short-description/ convention
used by this repo. This breaks experiment artifact naming consistency and related tooling/lint
expectations.
Agent Prompt
## Issue description
The experiment was added under `eval-statistical-significance/`, which does not match the required experiment artifact naming convention `NNNN-short-description/`.

## Issue Context
The repository’s experiment conventions (and lint tooling) expect a zero-padded 4-digit numeric prefix for each experiment directory.

## Fix Focus Areas
- eval-statistical-significance/EXPERIMENT.md[1-81]
- README.md[33-41]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Comment on lines +1 to +5
# Experiment: Statistical significance for non-deterministic agent evals

**Status:** Complete (v1)
**Relates to:** [fullsend-ai/fullsend#2460](https://github.com/fullsend-ai/fullsend/issues/2460) · `testing-agents.md` open question #1 · `experiments/promptfoo-eval` · `experiments/code-agent-evaluation`

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Action required

2. experiment.md missing yaml frontmatter 📘 Rule violation § Compliance

The experiment’s primary markdown document does not start with YAML frontmatter containing at least
title and status. This violates the experiment documentation metadata requirement.
Agent Prompt
## Issue description
`eval-statistical-significance/EXPERIMENT.md` begins directly with an H1 and does not include the required YAML frontmatter block with non-empty `title:` and `status:` fields.

## Issue Context
Existing experiments in this repo use YAML frontmatter at the top of the main experiment markdown (see README conventions).

## Fix Focus Areas
- eval-statistical-significance/EXPERIMENT.md[1-10]
- README.md[37-39]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Comment on lines +1 to +4
# Experiment: Statistical significance for non-deterministic agent evals

**Status:** Complete (v1)
**Relates to:** [fullsend-ai/fullsend#2460](https://github.com/fullsend-ai/fullsend/issues/2460) · `testing-agents.md` open question #1 · `experiments/promptfoo-eval` · `experiments/code-agent-evaluation`

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Action required

3. Readme missing experiment row 📘 Rule violation ≡ Correctness

A new experiment artifact was added but the root README experiment index table was not updated to
include it. This leaves the index out of sync with experiments on disk.
Agent Prompt
## Issue description
The root `README.md` experiment index table does not include an entry for the newly added experiment.

## Issue Context
The README is the canonical index of experiments and is expected to have exactly one row per experiment artifact present on disk.

## Fix Focus Areas
- README.md[5-32]
- eval-statistical-significance/EXPERIMENT.md[1-5]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Comment on lines +81 to +96
def build_parser() -> argparse.ArgumentParser:
parser = argparse.ArgumentParser(description=__doc__.splitlines()[0])
parser.add_argument("results", help="path to a promptfoo results JSON file")
parser.add_argument(
"--target", type=float, required=True,
help="minimum acceptable pass rate, e.g. 0.90",
)
parser.add_argument(
"--confidence", type=float, default=0.95,
help="confidence level for the interval (default 0.95)",
)
return parser


def main(argv: list[str] | None = None) -> int:
args = build_parser().parse_args(argv)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remediation recommended

4. build_parser single-use helper 📘 Rule violation ⚙ Maintainability

The new build_parser() abstraction is introduced but is only used once by main() and does not
replace duplicated logic. This adds an extra abstraction layer without clear reuse benefit.
Agent Prompt
## Issue description
`build_parser()` is a single-use helper that currently only serves one call site and doesn’t reduce duplication.

## Issue Context
The compliance rule aims to avoid introducing new abstractions that aren’t reused or simplifying existing repetition.

## Fix Focus Areas
- eval-statistical-significance/threshold_check.py[81-96]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Comment on lines +130 to +136
def __str__(self) -> str: # pragma: no cover - display only
verdict = "PASS" if self.passed else "FAIL"
return (
f"{verdict}: {self.observed_rate:.1%} observed over {self.n} trials "
f"(95% CI [{self.lower_bound:.1%}, {self.upper_bound:.1%}], "
f"target {self.target_rate:.1%})"
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remediation recommended

5. Hardcoded 95% ci label 🐞 Bug ◔ Observability

ThresholdResult.__str__ always prints “95% CI” even when threshold_test() is called with a different
confidence (e.g. via threshold_check.py --confidence), making CLI/log output misleading.
Agent Prompt
### Issue description
`ThresholdResult.__str__` hardcodes the string `95% CI` even though the interval is computed using the `confidence` parameter. This makes `threshold_check.py` output misleading whenever `--confidence` is not 0.95.

### Issue Context
The CLI supports `--confidence`, and `threshold_test(..., confidence=...)` uses that value to compute the Wilson interval; only the display string is wrong.

### Fix Focus Areas
- eval-statistical-significance/significance.py[119-136]
- eval-statistical-significance/threshold_check.py[88-108]

### Suggested fix
- Add a `confidence: float` field to `ThresholdResult` and populate it in `threshold_test`.
- Update `__str__` to print `{confidence:.0%} CI` (or `{confidence*100:.1f}%`) instead of hardcoding 95%.
- (Optional consistency) Apply the same approach to `ComparisonResult.__str__` as well.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Comment on lines +171 to +201
def bootstrap_ci(
samples: Sequence[float],
confidence: float = 0.95,
iterations: int = 10_000,
statistic: Callable[[Sequence[float]], float] = _mean,
seed: int | None = None,
) -> tuple[float, float]:
"""Percentile bootstrap confidence interval for an arbitrary statistic.

Makes no normality assumption, which matters for bounded judge scales
(1-5) where the sampling distribution is skewed near the ceiling.

`seed` is required for reproducible CI runs; leave it None for analysis.
"""
if len(samples) < 2:
raise ValueError("need at least 2 samples to bootstrap")
if iterations < 1:
raise ValueError(f"iterations must be positive, got {iterations!r}")

rng = random.Random(seed)
n = len(samples)
resampled = [
statistic([samples[rng.randrange(n)] for _ in range(n)])
for _ in range(iterations)
]
resampled.sort()

alpha = 1.0 - confidence
lo_idx = int(math.floor((alpha / 2.0) * iterations))
hi_idx = min(int(math.ceil((1.0 - alpha / 2.0) * iterations)) - 1, iterations - 1)
return resampled[lo_idx], resampled[hi_idx]

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remediation recommended

6. Bootstrap params not validated 🐞 Bug ☼ Reliability

bootstrap_ci() and compare_means() don’t validate confidence, and compare_means() also doesn’t
validate iterations, so invalid inputs can silently produce nonsensical/inverted intervals (e.g.,
confidence>1 yields negative indices) or crash (iterations<=0 yields IndexError from empty diffs).
Agent Prompt
### Issue description
`bootstrap_ci()` and `compare_means()` lack input validation for `confidence` (and `compare_means()` also lacks validation for `iterations`). Because percentile indices are derived directly from these parameters, out-of-range values can:
- silently compute incorrect bounds (e.g., negative indices when `confidence > 1.0`), or
- crash at runtime (e.g., `iterations <= 0` makes `diffs` empty in `compare_means`, then indexing raises `IndexError`).

### Issue Context
Other helpers already validate similar parameters (e.g., `_z_two_sided` validates confidence), so these APIs should be consistent and fail fast with `ValueError`.

### Fix Focus Areas
- eval-statistical-significance/significance.py[171-202]
- eval-statistical-significance/significance.py[224-271]

### Suggested fix
- In both `bootstrap_ci` and `compare_means`, add `if not 0.0 < confidence < 1.0: raise ValueError(...)`.
- In `compare_means`, add `if iterations < 1: raise ValueError(...)`.
- (Optional) Add unit tests asserting `ValueError` for confidence outside (0,1) and iterations<=0 in `compare_means`.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Comment on lines +47 to +76
if stats is not None and "successes" in stats:
successes = int(stats["successes"])
failures = int(stats.get("failures", 0))
total = successes + failures
if total <= 0:
raise ValueError("stats block reports zero trials")
return successes, total

# Shape 2: a per-result array with `success` booleans.
rows = None
if isinstance(root, dict) and isinstance(root.get("results"), list):
rows = root["results"]
elif isinstance(root, list):
rows = root
if rows is not None:
total = len(rows)
if total == 0:
raise ValueError("results array is empty")
successes = 0
for row in rows:
if isinstance(row, dict):
if "success" in row:
successes += 1 if row["success"] else 0
elif "pass" in row:
successes += 1 if row["pass"] else 0
else:
raise ValueError("result row has no 'success'/'pass' field")
else:
raise ValueError("result row is not an object")
return successes, total

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remediation recommended

7. Truthiness miscounts successes 🐞 Bug ≡ Correctness

extract_counts() increments successes based on Python truthiness for row['success']/row['pass'] and
coerces stats counts via int(), so malformed JSON types (e.g., "false" as a string, or non-integer
counts) can be misinterpreted and produce an incorrect CI gate verdict.
Agent Prompt
### Issue description
`extract_counts()` currently treats any truthy value as a success (e.g., the string `'false'` is truthy and would be counted as success) and silently truncates non-integer stats values via `int(...)`. This can lead to incorrect success/total counts and therefore incorrect threshold gating.

### Issue Context
This tool is intended to be a CI gate. When the input JSON is malformed or has unexpected types, it’s safer to fail closed (raise `ValueError`) than to guess.

### Fix Focus Areas
- eval-statistical-significance/threshold_check.py[41-76]

### Suggested fix
- For stats shape:
  - Require `type(stats['successes']) is int` and `type(stats.get('failures',0)) is int` (reject floats/strings/bools).
  - Validate non-negative counts and `total > 0`.
- For row shape:
  - Require `type(row['success']) is bool` / `type(row['pass']) is bool`.
  - Raise `ValueError` if types are not booleans.
- Add a small unit test showing that `'false'` (string) is rejected rather than counted as success.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant