U of I × MindRouter App Gallery

Study Power Lab

Research Built with ForgeCode v2.13.21 Critic status: shipped

Study Power Lab

Sample size and statistical power, visually

Figure out how many participants, plots or animals your study needs — and see why — with interactive power curves for the tests researchers actually use.

Study Power Lab running on a desktop screen, sample size and statistical power, visually Desktop · click to enlarge
Study Power Lab running on a phone, sample size and statistical power, visually
Phone · click to enlarge

What it is ✎ blurb written by the AI critic

Study Power Lab works out how many participants, plots or animals a study needs for six common tests: two-sample and paired t, one-way ANOVA, two proportions, correlation and chi-square goodness of fit. Choose to solve for sample size, power or the smallest detectable effect, and the power curve and the null-versus-alternative plot update as you type, with dropout and unequal group sizes included. Every number is computed in the page and checked against 32 reference cases from statsmodels, SciPy and the G*Power manual.

Try this

  1. Switch Solve for to Effect size to see the smallest difference 50 people per group can detect with 80% power (d = 0.566), then press Copy on the methods sentence.
  2. Set the allocation ratio to 2 and the dropout to 0.2, then check how many people to enroll in each group and download the CSV power table.
  3. Type group means such as 10, 12, 14 and a within-group SD of 5 into the Cohen's f helper. The app switches to one-way ANOVA and sizes the study.

Highlights ✎ written by the AI critic

  • Solves for sample size, power or detectable effect size across six tests, using noncentral t, F and chi-square distributions
  • Power curve with a hover or tap readout, plus a null-vs-alternative plot with the alpha and beta regions shaded
  • Dropout inflation and unequal group allocation carry through to the result, the copyable methods sentence and the CSV power table
  • Effect-size helpers turn means, SDs and proportions into Cohen's d, f and h with small/medium/large guidance
  • A validation panel re-runs 32 reference cases (statsmodels, SciPy, G*Power manual) in the browser, and all 32 pass

Who it's for

Graduate students, IRB/IACUC protocol writers, faculty planning experiments

Features

  • Test chooser: two-sample t, paired t, one-way ANOVA, two proportions, correlation, chi-square goodness-of-fit
  • Solve for any one of: sample size, power, detectable effect size — given alpha and the others
  • Live power curve (power vs n) and a distribution plot showing null vs alternative with α and β regions shaded
  • Effect-size helpers (Cohen's d from means/SD, f from group means, h from proportions) with plain-language small/medium/large guidance
  • Attrition/dropout inflation and unequal allocation ratio
  • Copyable methods-section sentence and CSV of the power table

Data & sources

  • data/raw/reference_values.json: validation oracle of 32 cases, computed with statsmodels 0.15.0 and SciPy 1.14.1 (derived, not downloaded), with selected values transcribed from the G*Power 3.1 manual (June 1, 2023). Embedded by the harness's scripts/build_validation_data.py as data/embed/validation.json
  • doi.org — Cohen, J. (1988). Statistical Power Analysis for the Behavioral Sciences (2nd ed.).
  • doi.org — Cohen, J. (1992). A power primer. Psychological Bulletin 112(1).
  • www.psychologie.hhu.de — G*Power 3.1 manual:
  • doi.org — Faul et al. (2007)
  • www.statsmodels.org — statsmodels power docs
Full data provenance ✎ written by the AI reviewer

Data sources: Study Power Lab (power-calculator)

Prepared 2026-09-13. The app needs no external dataset: all power and sample-size math is implemented in JavaScript. This directory holds a validation oracle (reference values the app's "validation" panel and tests can compare against) plus the links the "Data & sources" panel should cite.

DERIVED DATA. The numbers in reference_values.json were computed with statsmodels 0.15.0 and SciPy 1.14.1. They were not copied from published tables. There are two exceptions, and both are labelled inside the file:

  1. Fields named published_reference hold output values transcribed verbatim from the G*Power 3.1 manual (June 1, 2023), with section and page numbers. They are there so the computed values can be checked against published G*Power output.

  2. effect_size_benchmarks holds Cohen's small/medium/large conventions. They come from Cohen (1988) and are summarized in Cohen (1992) Table 1. Verification status: d (.2/.5/.8), r (.1/.3/.5) and f (.10/.25/.40) were checked against the text of the G*Power 3.1 manual, which quotes Cohen (pp. 9, 24, 50, 52). h (.2/.5/.8) and w (.1/.3/.5) are the standard Cohen conventions, but they were not checked against a retrieved copy of Cohen (1988) or (1992), because neither book nor article was available to this data prep. The same status is recorded per index in effect_size_benchmarks.verified_against_retrieved_source.

No downloads are stored here. The G*Power manual PDF was fetched only into a scratch directory outside this repository so its example outputs could be transcribed. It is not redistributed.


File 1: reference_values.json

FieldValue
URLNone. Computed locally, not downloaded.
PublisherComputed for the University of Idaho gallery data prep with open-source libraries: statsmodels (statsmodels developers) and SciPy (SciPy developers)
Retrieval / computation date2026-09-13
Generatorcompute_reference_values.py (File 2), run as venv/bin/python compute_reference_values.py reference_values.json
EnvironmentPython 3.11.5, macOS-14.5-arm64; numpy 2.2.6, scipy 1.14.1, statsmodels 0.15.0 (throwaway venv: python3 -m venv venv && venv/bin/pip install scipy==1.14.1 statsmodels)
Records32 cases in cases[]. Tests: two-sample t (7 + 1 attrition case), paired t (6), one-way ANOVA (4), two proportions (5), correlation (5), chi-square goodness of fit (4). Each test has sample-size, power and effect-size solves.
Date rangeNot applicable. These are time-independent reference values, computed 2026-09-13.
FormatUTF-8 JSON object, indent 1
License / termsComputed facts generated for this project, free to reuse. Libraries used: statsmodels (BSD-3-Clause) and SciPy (BSD-3-Clause). Transcribed G*Power numbers are short factual excerpts with a citation.
How it was computed (exact method)
  • Primary value (statsmodels_value): the literal Python expression in statsmodels_call was run with eval. The recorded call string is therefore exactly the code that produced the number.

    • Example (T1): statsmodels.stats.power.TTestIndPower().solve_power(effect_size=0.5, nobs1=None, alpha=0.05, power=0.8, ratio=1.0, alternative='two-sided') gives 63.765611775409695.

  • Independent cross-check (scipy_value): a separate implementation of the textbook formulas using scipy.stats (nct, ncf, ncx2, norm). Solves use scipy.optimize.brentq(xtol=1e-12, rtol=1e-14). The formula is described in scipy_method, and abs_diff_statsmodels_vs_scipy gives the difference.

    • Maximum differences: power and effect size about 2e-6. Continuous n about 1e-4, except ANOVA total N, which differs by up to 0.0012. That gap comes from statsmodels' own noncentral-F survival function versus scipy.stats.ncf.sf.

  • Correlation sample-size solves (R1, R2). In statsmodels 0.15.0, NormalIndPower(ddof=3).solve_power(nobs1=None, ratio=0) does not converge: it returns 3.0000004 after taking the square root of a negative nobs-3 while bracketing. The statsmodels power function was therefore root-found with scipy.optimize.brentq, and that call is recorded in statsmodels_call. closed_form_value gives the textbook ((z_(1-a/2)+z_power)/zeta)^2 + 3. It ignores the opposite tail, so it is slightly larger.

  • Attrition (D1) is plain arithmetic, not a library value (arithmetic_only: true). Both common rounding orders are given because they disagree: 71 vs 72.

JSON layout (parsing notes)
  • Top level: title, derived (true), provenance, retrieval_or_computation_date, environment{}, conventions{} (formula and parameterization notes per test), effect_size_benchmarks{}, case_count, cases[].

  • cases[i] fields:

    • id: T1..T7, P1..P6, A1..A4, H1..H5, R1..R5, C1..C4, D1.

    • test: one of two_sample_t, paired_t, one_way_anova, two_proportions, correlation, chi_square_gof.

    • solve_for: one of sample_size, power, effect_size, sample_size_with_attrition.

    • label: human-readable description.

    • inputs{}: parameter names state their meaning, e.g. effect_size_d, effect_size_dz, effect_size_f, effect_size_h, rho_H1, rho_H0, fisher_z_effect, effect_size_w, alpha, power, n1, n2, n_pairs, total_n, k_groups, n_categories_k, df, allocation_ratio_n2_over_n1, alternative (two-sided or larger, i.e. one-tailed in the effect direction), p1, p2 (float proportions, H1/H3/H4), p_null, p_alt (lists of 4 category probabilities, C2 only). D1 alone uses n_per_group_required_continuous, dropout_rate (fraction, 0.1 = 10%) and from_case (string id, "T1").

    • value_meaning: what the value is, e.g. "n1 = size of group 1", "TOTAL N across all k groups", "number of pairs", "power (1 - beta)", "Cohen's f".

    • statsmodels_value (float), statsmodels_call (string), statsmodels_warnings (list; empty in this run).

    • scipy_value (float), scipy_method (string), abs_diff_statsmodels_vs_scipy (float).

    • at_integer_n{} (sample-size solves only): rounding_rule, the rounded design (n1/n2/total_n, n_pairs, or n_per_group + total_n), actual_power_scipy, actual_power_statsmodels (absent for proportions and correlation), plus critical value, df and noncentrality (critical_t, noncentrality_delta, critical_F, df_num, df_denom, noncentrality_lambda, critical_z, noncentrality_shift, fisher_z_difference, critical_chi2). These are useful for checking the null-vs-alternative distribution plot.

    • details{} (power solves): the same critical value, df and noncentrality fields.

    • Optional blocks:

      • effect_size_h{} / effect_size_w{} (H1, H3, H4 / C2): the effect size computed from proportions, with its own call (statsmodels_value, statsmodels_call; H1, H3 and C2 also carry scipy_value, scipy_method; H4 does not).

      • published_reference{source, transcribed_values, note?} on P2, P3, P4, P5, A2, R2, R4. note is present only on P4, A2, R2 and R4. The keys of transcribed_values are **free-text G*Power output labels, not a fixed schema**, and their capitalization differs between cases ("Actual power" in P4/P5 vs "Actual Power" in A2; R2 uses "exact method Total sample size", "exact method Actual power", "large sample (z) approximation sample size N"; R4 uses "exact power" and "approximation power"). Match them by case or compare case-insensitively. Values are numbers (integers for df and sample sizes).

      • note (string) at case level: R1, R2 (statsmodels non-convergence) and D1 (rounding orders).

      • closed_form_value, closed_form_method: R1 and R2.

      • statsmodels_fisher_z_value: R5 (the value is r = tanh(z)).

      • arithmetic_only: true and variants{}: D1 only. D1 has no statsmodels_value/scipy_value; its answers are variants.ceil_of_continuous_over_retention.value (71) and variants.ceil_of_integer_over_retention.value (72), each with formula and python strings.

  • Units and conventions:

    • alpha and power are probabilities in [0, 1].

    • Sample sizes are counts. *_value fields for sample-size solves are continuous solutions. Designs are rounded up in at_integer_n, which is the value G*Power reports ("Total sample size", "Actual power").

    • Two-sided power includes the tiny wrong-sign rejection tail, as statsmodels and G*Power do.

    • ANOVA statsmodels nobs is total N. Its rounding is n per group = ceil(N/k).

    • JSON number types: rounded counts in at_integer_n (n1, n2, total_n, n_pairs, n_per_group) are integers; degrees of freedom (df, df_num, df_denom) in at_integer_n/details are floats (126.0). Input alpha/power/effect sizes are floats; input n1/n2/n_pairs/total_n/k_groups are integers.

  • Missing values: none. A key is simply absent when it does not apply to a case.

  • Suggested test tolerances: continuous n within 0.05 (or an exact match after ceil); power within 5e-4; effect sizes within 1e-3.

Headline values
CaseInputsComputed
T1two-sample t, d = 0.5, alpha .05 two-sided, power .80n = 63.7656 per group (64 + 64, actual power 0.80146)
T4same as T1 with allocation 2:1n1 = 47.742 (48 + 96)
P1paired t, dz = 0.5, power .8033.367 pairs (34)
A1ANOVA, f = 0.25, k = 3, power .80total N = 157.19 (53 per group, N = 159, power 0.80489)
H1proportions .65 vs .45h = 0.404860, n = 95.769 per group (96)
R1correlation, r = .30, rho0 = 0, power .80N = 84.928 (85)
C1chi-square GOF, w = 0.3, k = 4 (df 3), power .80N = 121.140 (122)
Cross-check against published G*Power output (G*Power 3.1 manual, June 1 2023)
CaseManual locationPublishedComputed here
P2sec. 19.3, p. 48 (matched pairs, dz = 0.421637, N = 50)delta 2.981424, crit t 2.009575, power 0.832114delta 2.981424, crit t 2.009575, power 0.832114
P3sec. 19.3, p. 48 (dz = 0.2828427, N = 50)power 0.5003520.500352
P4sec. 20.3, p. 50 (one-sample, one tail, d = 0.625, power .95)N 30, delta 3.423266, crit t 1.699127, power 0.955144N 30, 3.423266, 1.699127, 0.955144
P5sec. 20.3, p. 50 (two tails, d = 0.1, alpha .01, power .90)N 1492, delta 3.862642, power 0.900169N 1492, 3.862642, 0.900169
A2sec. 10.3, p. 25 (f = .25, k = 10, power .95)N 390, lambda 24.375, crit F 1.904538, power 0.952363N 390, 24.375, 1.904538, 0.952363 (scipy) / 0.952364 (statsmodels)
R2sec. 3.3, p. 9 (rho0 = .60 vs rho = .65, power .95)exact N 1928; z approximation N 1929Fisher z: N 1929
R4sec. 3.3, p. 9 (rho0 = .80, rho = .30, N = 8)exact 0.482927; approximation 0.422599Fisher z: 0.422599

The one-sample t chapter (P4, P5) validates the paired-t engine, because a paired t test is a one-sample t test on the differences. The G*Power manual has no worked example for two independent means (section 21.3 is empty), the chi-square goodness-of-fit test, or the arcsine two-proportion test. Those cases rely on the statsmodels-vs-scipy agreement only.

Independent verification (verifier pass, 2026-09-13)
  • JSON parses; case_count = 32 = len(cases); every test has sample-size, power and effect-size solves; statsmodels_warnings is empty in all 31 cases that have it.

  • All 37 recorded call strings (31 statsmodels_call, 4 effect-size calls, 2 D1 python strings) were re-run with eval in the same venv (statsmodels 0.15.0, SciPy 1.14.1, numpy 2.2.6). All 37 reproduce the stored values exactly. The statsmodels 0.15.0 correlation solve_power non-convergence (3.00000036) was reproduced.

  • A third, independent implementation using only mpmath 1.3.0 (30-digit precision) was also checked. It uses no SciPy and no statsmodels: noncentral t by quadrature over the chi-square mixing density, and noncentral F and chi-square as Poisson mixtures. It agrees on T1, T4, T6, T7, P2, P4, P5, A1, A2, A3, C1, C2, C3, H1, R1, R2, R4 and R5. Differences are at most 1.6e-6 in continuous n, and at most 5e-7 against the 6-decimal G*Power values.

  • Every at_integer_n.actual_power_scipy is at or above its target power.

  • Every published_reference number was found in the manual text on the stated PDF page (9, 25, 48, 50) and in the stated section (3.3, 10.3, 19.3, 20.3). The inputs match: dz 0.421637 and 0.2828427 with N = 50; d 0.625 one tail; d 0.1 at alpha .01; f .25, k = 10; rho0 .60 vs .65; rho0 .8 vs .3 at N = 8. Section 21.3 (two independent means) is empty in the manual. The manual lists d, f, r (and f2, q, g, rho2) conventions but no h or w conventions, which is consistent with the verified_against_retrieved_source flags.

  • The manual PDF was re-fetched live (HTTP GET, 200, application/pdf) and is byte-identical to the copy used for transcription: 3,664,779 bytes, SHA-256 cde1293c99dac2a5da7fa9cf660fb3c1ad16e58fdc973823545cd3ec40c8249f. All 21 transcribed values and phrases were found in pdftotext -layout output of the live copy. The PDF is still not stored here.

  • All citation links below were re-checked with HTTP GET on 2026-09-13. The results match the statuses listed, including the 403 from doi.apa.org for Cohen (1992).

Suggested citation (Data & sources panel)
Validation values computed with statsmodels 0.15.0 and SciPy 1.14.1 (not copied from published tables); selected cases checked against G*Power 3.1 manual examples (Faul et al., 2007).

File 2: compute_reference_values.py

FieldValue
URLNone. Written locally for this data prep.
Publisher / dateUniversity of Idaho gallery data prep, 2026-09-13
ContentsThe exact script that produced File 1. Kept for reproducibility. Not part of the app build. It needs numpy, scipy and statsmodels, so it does not follow the app's standard-library-only rule for scripts/.
Records / date rangeNot applicable
FormatPython 3 source. Run with python compute_reference_values.py OUT.json.
LicenseFree to reuse
Suggested citationNot needed. Cite File 1.

Citation links for the app's "Data & sources" panel (links only; nothing downloaded here)

Each link was checked with an HTTP GET on 2026-09-13.

  • Cohen, J. (1988). Statistical Power Analysis for the Behavioral Sciences (2nd ed.). Routledge. https://doi.org/10.4324/9780203771587. Resolved (200) to Taylor & Francis. Origin of the effect-size conventions: d .2/.5/.8, f .10/.25/.40, h .2/.5/.8, r .1/.3/.5, w .1/.3/.5.

  • Cohen, J. (1992). A power primer. Psychological Bulletin, 112(1), 155–159. https://doi.org/10.1037/0033-2909.112.1.155. Table 1 summarizes the conventions above. The DOI redirects to doi.apa.org, which answered 403 to a scripted request (bot blocking). The DOI itself is valid; open it in a browser.

  • **G*Power** (Heinrich-Heine-Universität Düsseldorf), software and documentation page: https://www.psychologie.hhu.de/arbeitsgruppen/allgemeine-psychologie-und-arbeitspsychologie/gpower (200)

  • **G*Power 3.1 manual** (June 1, 2023, 86 pp., PDF): https://www.psychologie.hhu.de/fileadmin/redaktion/Fakultaeten/Mathematisch-Naturwissenschaftliche_Fakultaet/Psychologie/AAP/gpower/GPowerManual.pdf (200, application/pdf, 3,664,779 bytes)

  • Faul, F., Erdfelder, E., Lang, A.-G., & Buchner, A. (2007). G*Power 3: A flexible statistical power analysis program for the social, behavioral, and biomedical sciences. Behavior Research Methods, 39, 175–191. https://doi.org/10.3758/BF03193146 (200)

  • Faul, F., Erdfelder, E., Buchner, A., & Lang, A.-G. (2009). Statistical power analyses using G*Power 3.1: Tests for correlation and regression analyses. Behavior Research Methods, 41, 1149–1160. https://doi.org/10.3758/BRM.41.4.1149 (200)

  • statsmodels power and sample-size documentation: https://www.statsmodels.org/stable/stats.html#power-and-sample-size-calculations (200)

  • SciPy statistics documentation: https://docs.scipy.org/doc/scipy/reference/stats.html (200). Citation: Virtanen et al. (2020), Nature Methods 17, 261–272, https://doi.org/10.1038/s41592-019-0686-2 (200, resolved to nature.com on the verifier's re-check, 2026-09-13).

  • R package pwr (a common implementation of Cohen's methods, for reference): https://cran.r-project.org/package=pwr (200). It is not installed on this machine and was not used to compute anything.

How it was built

Written by ForgeCode 2.13.21 (forgecode.dev) on qwen/qwen3.8-27b, served by MindRouter, the University of Idaho's on-premises AI platform.

13harness rounds
127.7minutes of harness work (2.1 h)
2,578lines of code
179KB, single-file app

Rounds by kind

build1
feedback8
continue2
critic2

Lines of code per file

body html249
app css444
app js1,782
scripts103

Critic scores ✎ scored by the independent AI critic, 1–10

visual 8 out of 10
usefulness 9 out of 10
correctness 9 out of 10
interactivity 8 out of 10

Independent audits ✎ by the AI critic

Data honesty: checked against raw data Authorship audit: every app-code commit is a harness round

Automated checks

Console no console errors No network 0 external requests Phone layout 0 px overflow Canvases no empty canvases Brand header header present No key leak no key in source

Checked by critic-verify.

Data spot checks ✎ written by the AI critic

  • Default two-sample t (d 0.5, alpha .05 two-sided, power .80): app 63.77 per group -> 64 + 64, power at rounded design 80.1%; SciPy 63.7656 and 0.80146; data/raw/reference_values.json T1 statsmodels 63.765612 (64 + 64, actual power 0.80146)
  • Attrition, 10% dropout on the default design: app 'enroll ~ 71 per group'; by hand ceil(63.7656 / 0.90) = 71; reference_values.json D1 variant ceil_of_continuous_over_retention = 71
  • Unequal allocation, ratio 2: app 47.74 -> 48 + 96 (total 144), enroll 54 + 107; SciPy n1 = 47.742; reference_values.json T4 n1 = 47.742 (48 + 96); by hand ceil(47.742/0.9) = 54, ceil(95.484/0.9) = 107
  • Two-sample t, d 0.4, alpha .01 one-sided, power .90, equal groups (hero view): app 164.07 -> 165 + 165, critical t 2.3378, noncentrality 3.6332, power 90.2%, enroll 183; SciPy 164.072, 2.33777, 3.63318, 0.90179, ceil(164.072/0.9) = 183
  • Same test with ratio 2: app 122.94 -> 123 + 246, critical t 2.3366, 90.0%; SciPy 122.940, 2.33655, 0.90015
Show all 17 spot checks
  • Paired t, 6 pairs, dz 1.2, Power mode: app 65.5%, critical t 2.5706, legend alpha 5.0% / power 65.5%; SciPy nct power 0.65504, t crit 2.57058
  • One-way ANOVA f 0.25, k 3: app N = 157.19 -> 53 x 3 = 159, power 80.5%; SciPy 157.1898; reference_values.json A1 157.19 (53 per group, power 0.80489)
  • One-way ANOVA f 0.3, k 4: app N = 125.12 -> 32 x 4 = 128, power 81.0%; SciPy 125.117, power at 128 = 0.81020
  • Chi-square GOF w 0.2, k 5: app 298.38 -> 299, power 80.1%; SciPy 298.382, power at 299 = 0.80093
  • Two proportions 0.65 vs 0.45: app h = 0.405, 95.77 per group -> 96 + 96; SciPy h 0.404860, n 95.769; reference_values.json H1 h 0.404860, n 95.769
  • Two proportions 0.30 vs 0.20, ratio 1.5: app 243.07 -> 244 + 365, enroll 271 + 406; SciPy n1 243.074, n2 ceil(364.61) = 365, ceil(243.074/0.9) = 271, ceil(364.61/0.9) = 406
  • Correlation r 0.3 vs rho0 0: app N = 84.93 -> 85; SciPy Fisher-z 84.928; reference_values.json R1 84.928
  • Correlation r 0.7 vs rho0 0.5: app N 80.62, power at N 60 = 67.0%, detectable r (Effect size mode) 0.726; SciPy 80.619, 0.67034, 0.72608
  • Effect-size mode, two-sample t, 50 + 50, power .80: app detectable d = 0.566 with curve marker 'power = 80.0%'; SciPy 0.56588
  • Effect-size helpers: f from means 10, 12, 14 with within-SD 5 -> app 0.327 (hand: sqrt(8/3)/5 = 0.32660); h from 0.30 vs 0.20 -> app 0.232 (hand: 2asin(sqrt .3) - 2asin(sqrt .2) = 0.23198)
  • Validation panel re-runs all 32 cases from data/embed/validation.json in the browser: 32 of 32 within tolerance; G*Power manual transcription shown for A2 (lambda 24.375, critical F 1.904538, N 390, actual power 0.952363) matches data/raw/reference_values.json
  • Sources: the visible Data & sources section links Cohen 1988 and 1992 (DOIs), the G*Power page and manual, Faul et al. 2007/2009, statsmodels, SciPy and R pwr; it states that validation values were computed with statsmodels/SciPy rather than copied from published tables. No University of Idaho figures or synthetic datasets are presented.
Build notes ✎ written by the AI reviewer

I never edited src/, scripts/, data/embed/ or index.html. All app code came from ForgeCode 2.13.21 rounds through run_round.sh, and the tamper guard never fired. Data prep had already committed data/raw (verdict ready-after-repair), so I downloaded nothing.

  • Rounds: 11 in total (1 build, 8 feedback, 2 continue), 121.9 harness minutes, no network errors. Round 1 (build, 29 min) produced a nicely designed page, but the statistics were badly wrong: 15/32 validation, broken noncentral t and chi-square, all solves stuck at their lower bound, NaN console errors. It ended at ForgeCode's 100-request-per-turn cap.

  • Rounds 2-4 produced no app changes:

    • Round 2 spent its 100 requests on 96 scratch node experiments with the noncentral-t CDF.

    • Round 3 (continue) degenerated into hundreds of numbered variables (p0...p830) in a Python heredoc and hit the tool-failure limit.

    • Round 4 spent 95 requests printing validation.json one case at a time.

  • After that I changed my feedback style. Round-4 feedback gave the function names, line numbers and exact wrong terms, still without code:

    • engineParams() never passed the target power to the solvers.

    • chi2Cdf used the wrong formula.

    • Later: a spurious -df/2 term in chi2Pdf and in nctCdf's log constant, which I confirmed with my own node probe outside the project.

    • I also pointed to standard methods (regularized incomplete gamma, Simpson integration of the noncentral-t definition). Please note that level of guidance when judging how independently the harness built this.

  • Rounds 5-6 fixed the statistics (32/32). Rounds 7-9 did the UI work: β shading, legend, helper output, hover tooltip, validation, phone cards, sticky bar. Each round introduced and later fixed a regression: an ANOVA ReferenceError: p is not defined, phone validation rows hidden, and x-axis range jumping to 2,000.

  • Round 10, the 8th and last feedback round, hit the request cap with no app changes. As with round 3, I ran round 11 as a continue round with the same prompt and pointed FEEDBACK.md at the exact lines. It fixed the x-range and changed touch-action from none to pan-y.

  • verify-r10 was not run because index.html was byte-identical to round 9.

  • One target in my own round-5 feedback was wrong (r = 0.341); the harness correctly kept 0.355.

  • Other harness behaviour: Round 7 created a jsdom scratch directory at /tmp/spd-test outside the project.

  • Round 8 launched the system Google Chrome headless about 20 times to dump the DOM.

  • Three Chrome processes with Playwright-style flags were running on the machine. They are not from my scripts (mine use bundled Chromium and close it), so I left them alone.

I removed FEEDBACK.md and committed the deletion (24c8924). No local servers are left on ports 18880-18889. Scripts, probes and all screenshots are in runs/power-calculator.

Round-by-round timeline

Every harness round, in order. The prompts and FEEDBACK.md were written by the AI reviewer; the app code was written by the harness. Expand a round to see the exact prompt and the FEEDBACK.md the harness received.

    1 build 29.5 min · 6 files changed, 5519 insertions(+), 3 deletions(-) Show prompt

    Prompt given to the harness:

    Read AGENTS.md, SPEC.md and BRAND.md in this directory. Then build the complete app they describe, step by step: first write a small Python standard-library script in scripts/ that reads the validation oracle data/raw/reference_values.json (see data/raw/SOURCES.md for its layout; do NOT copy or run data/raw/compute_reference_values.py, it needs non-stdlib packages) and writes a compact JSON of the validation cases (id, test, solve_for, label, inputs, expected value, integer-n design and any published G*Power values) plus the Cohen effect-size benchmarks into data/embed/, and run it; the app must compute every statistic itself in JavaScript and show its own result next to each expected reference value in a 'Validation' panel, and cite the links listed in data/raw/SOURCES.md in a visible 'Data & sources' section. Then write src/body.html, src/app.css and src/app.js; run python3 build.py --check and fix any error it reports. Implement every required feature in SPEC.md with a polished, modern, responsive University of Idaho design. Do not stop until python3 build.py --check succeeds and every feature is implemented.
    

    No feedback file was sent in this round.

    3 continue 15.4 min · 1 file changed, 11 insertions(+) Show prompt

    Prompt given to the harness:

    Read FEEDBACK.md in this directory — it lists problems a reviewer found when testing index.html in a real browser. Fix every item, keeping everything that already works, then run python3 build.py --check.
    

    FEEDBACK.md the harness received ✎ written by the AI reviewer:

    Reviewer findings — round 1 (tested index.html in headless Chromium, 1440 px and 390 px)

    Important — none of these items has been fixed yet. The previous fix attempt spent its entire 100-request budget on ~96 throwaway node -e experiments (93 successive variants of a noncentral-t CDF, none matching the expected values) and never edited src/app.js, so index.html is unchanged. Work incrementally this time: save each fix into src/app.js as soon as it works, run python3 build.py --check after each item, and keep exploratory experiments to a handful. For the noncentral t distribution, a well-known reliable method is Lenth (1989), Applied Statistics Algorithm AS 243 (Poisson-weighted sums of regularized incomplete beta terms, summed outward from the mode). Exact targets to test against: critical t 1.984467, df 98, noncentrality 2.5 → two-sided power 0.696893 (T6); critical t 2.009575, df 49, noncentrality 2.981424 → 0.832114 (P2). Get the statistics right first (items 1–5), then do items 6–15.

    The layout, validation panel, sources list, CSV download and Copy button are a good start. The statistics engine and the solvers are the big problem: most results a user sees are wrong. Fix items 1–5 first (verify with node against data/embed/validation.json before touching the UI), then the rest.

    1. "Solve for: Sample size" is broken for every test. With the defaults (two-sample t, d = 0.5, α = 0.05 two-sided, power 0.8, ratio 1, dropout 0) the result reads "2 per group (continuous) → 50 + 50 (total 100)". Expected "63.77 per group (continuous) → 64 + 64 (total 128), actual power 80.1%". The rounded design just echoes the old n inputs instead of ceil(continuous solution). Same pattern everywhere: paired t dz = 0.5 shows "3 pairs → 50 pairs" (expected 33.37 → 34 pairs); ANOVA f = 0.25, k = 3 shows "5 per group → 20 per group × 3 (total 60)" (expected N = 157.19 → 53 per group, N = 159); two proportions .65 vs .45 shows "2 per group → 100 per group" (expected 95.77 → 96 per group); correlation r = .30 shows "4 → 60 participants" (expected 84.93 → 85); chi-square w = 0.3, k = 4 shows "2 → 60" (expected 121.14 → 122). Changing the allocation ratio to 2 changes nothing (expected 47.74 → 48 + 96). The Validation panel passes A1/H1/R1, so the panel and the main UI evidently use different code paths — the main UI must use the same verified solver.

    2. "Solve for: Effect size" always shows 0 ("0 (Cohen's d)", "0 (Cohen's dz)", "0 (Cohen's f)", "0 (Cohen's h)", "0 (Fisher z difference)", "0 (Cohen's w)"). Expected e.g. two-sample t with 30 + 30, power .8, α .05 two-sided → d = 0.7356 (case T7); correlation should report r (not a Fisher-z difference) — N = 100, power .8 → r = 0.277 (R5).

    3. t-test power is wrong. Two-sample t, d = 0.5, n = 50 + 50, α .05 two-sided shows 33.8%; expected 69.7% (T6). Paired t, dz = 0.5, 50 pairs shows 82.3%; expected about 93.4%. The noncentrality shown for the two-sample case is 2.5 (correct), so the noncentral-t CDF/survival function itself is wrong. Validation cases T1–T7 and P1–P6 all FAIL (e.g. T1 app 93.42 vs expected 63.77).

    4. Chi-square goodness-of-fit is completely broken. The "Critical chi2" tile shows "357,303,691,362.7946" (df 2) and "100,940,668,751,159,450,000,000,000" (df 3); power is 0.0% at every N; the power curve is a flat line at 0; C1–C4 FAIL. Expected for df 3, α .05: critical χ² = 7.8147; w = 0.3, N = 122 → power ≈ 0.80. (The round-1 log shows the central χ² CDF was being computed through the incomplete beta function and did not match numeric integration — the χ² CDF is the regularized lower incomplete gamma P(df/2, x/2).) Selecting chi-square also produces console errors: Error: <path> attribute d: Expected number, "M52.00 NaN L54.05 NaN L…"., Error: <path> attribute d: Expected number, "M248.80 NaN L250.85 NaN …"., Error: <line> attribute y1: Expected length, "NaN". and ... y2 ... (the distribution plot gets NaN coordinates). The page must have zero console errors for every test and mode.

    5. Validation panel shows "15 of 32 cases within tolerance". After fixing 1–4 every case (T, P, A, H, R, C, D) must PASS with the tolerances in data/raw/SOURCES.md. Keep the red FAIL styling for genuine failures.

    6. Power curve is useless in sample-size mode: the x-axis only spans n = 2…20 and the curve hugs 0, with the marker stuck at "n = 2" (its label is drawn on top of the y-axis line). Expected: the x range extends past the point where power reaches ~0.95–0.99, the target-power line crosses the curve at the solved n, a gold marker + label sits at the solved/rounded n, and the plot animates smoothly when inputs change. Add a hover crosshair/tooltip that shows "n = …, power = …%" (currently hovering does nothing).

    7. Power table has only 2 rows in sample-size mode (n = 2 and 3, power 0.0%), so the CSV download is also just 2 rows ("n,power,beta / 2,0.000000,1.000000 / 3,0.000171,0.999829"). Expected a useful table around the answer (e.g. ~20–30 sensible n values from small to where power ≈ 0.99, including the solved n, highlighted), with total N and enrolled-with-attrition columns where relevant; the CSV should contain the same rows plus a header with the test, α, tails, effect size and ratio.

    8. Attrition text is wrong and inconsistent: "With 10% expected dropout, enroll ≈ 3 per group (ceil(2 ÷ 0.90))" — it inflates the broken n. Expected for T1: "enroll 71 per group (ceil(63.77 ÷ 0.90))", also showing the total to recruit. Changing dropout from 10% to 30% should visibly change the number.

    9. Methods sentence has bugs: "…(Cohen's d = 0.5, two-sided, α = 0.05 (two-sided)) requires n = 30 per group (total N = 60) to detect the effect with 12.0% power. Allowing for 15% attrition, enroll approximately 3 per group." — "two-sided" is duplicated with a stray double parenthesis, the enroll number contradicts n, and it should say which software/method was used (e.g. "computed with Study Power Lab using the noncentral t distribution"). For ANOVA and chi-square it must not say "two-sided".

    10. Stat tiles: the α tile's label renders as "A" (the uppercase text-transform turns "α" into a capital alpha that reads as the letter A); ANOVA and chi-square tiles say "0.05 (two-sided)" although F and χ² tests are one-tailed — hide the Tails control and the "(two-sided)" wording for those tests. "CRITICAL CHI2" should read "Critical χ²".

    11. No input validation / empty states: setting d = 0, d = −3 or target power = 1.5 leaves the old result on screen with no message. Expected an inline error under the field (e.g. "Power must be between α and 0.999", "Effect size must be non-zero") and the result card replaced by a friendly hint until inputs are valid.

    12. Effect-size helpers give no feedback: typing means 105 / 100 and SD 15 and clicking "Compute d" silently puts 0.3333333333333333 into the d field — no result is shown next to the helper and no plain-language guidance. Expected: a result line under each helper like "d = 0.33 — small-to-medium (Cohen: .2 small, .5 medium, .8 large)" with a small visual scale/marker, the value rounded to 3 decimals, and a button to "Use this value" that switches the test if needed (Compute f while the two-sample t test is selected currently writes f into a hidden field with no effect; Compute h does not update p₁/p₂ or the result). Empty or invalid helper inputs (blank mean, SD = 0) must show an inline message; they currently do nothing. The helper inputs are also too narrow — the third placeholder is cut off ("poole").

    13. Distribution plot legend collides with the critical-value label: at 1440 px and at 390 px the legend box ("null distribution / alternative (H₁) / α (reject H₀)") is drawn on top of the right-hand "critical t = 1.984" label. Move the legend outside the plot area (e.g. above it) and also shade the β region (the area of the alternative distribution that falls inside the acceptance region) in a second colour, as SPEC.md requires, with β listed in the legend.

    14. Phone (390 px): the power-curve and distribution-plot tick labels and axis titles are ~6 px tall and hard to read — scale the SVG text so it stays ≥ 11 px on phones (or draw with a phone-specific layout). The Validation table is wider than the card: the right-hand "This app" and "Status" columns are cut off with no visible cue; on phones show it as stacked cards or a clearly horizontally scrollable table with a hint.

    15. Polish: add subtle 150–250 ms transitions when the result number changes (e.g. count-up), keep the "How to use" box, and make sure every control has a visible focus ring. Re-read SPEC.md afterwards and confirm every required feature works for all six tests in all three solve modes.

    11 continue 2.2 min · 3 files changed, 93 insertions(+), 42 deletions(-) Show prompt

    Prompt given to the harness:

    Read FEEDBACK.md in this directory — it lists problems a reviewer found when testing index.html in a real browser. Fix every item, keeping everything that already works, then run python3 build.py --check.
    

    FEEDBACK.md the harness received ✎ written by the AI reviewer:

    Reviewer findings — round 9 build (tested index.html in headless Chromium at 1440 px and 390 px)

    Read this first — nothing below has been fixed yet. The previous attempt spent its whole 100-request budget on 98 shell commands (reading code, git show of older commits) and never edited src/app.js, so index.html is unchanged. The code locations are given below. Open src/app.js, make the two edits directly with the file-editing tool, then run python3 build.py --check. Do not explore git history.

    Round 9 fixed ANOVA attrition. The attrition box, methods sentence, sticky bar and CSV now all say 59 per group × 3 = 177. The ANOVA tooltip wording is fixed, the phone critical-value labels are fine, and Validation is still 32 of 32 with no console errors.

    However, the power-curve range change caused a visible regression on the very first chart visitors see, and touch support is not working yet. Fix only these items and change nothing else.

    Do not change any statistics, solver, attrition, methods-sentence, CSV, validation or layout code. Edit only the power-curve range logic and the power-curve pointer/touch handling in src/app.js.
    1. Regression: the power-curve x-axis now runs to 2,000 almost everywhere.

      • Cause: in the power-curve range code (src/app.js lines ≈ 968–978), the first upper bound is max(2.2 × n, n + 10, 20). When power at that bound is below 0.99 (raised from 0.95 in round 9), the next candidate is Math.max(nMax * 2, 2000). That jumps straight to at least 2,000 instead of growing gradually, so nearly every chart ends at 2,000.

      • Where: on first load (two-sample t, d 0.5, power .8) the x-axis runs from 0 to 2,000 per group. Power reaches 100% by n ≈ 525, so about three-quarters of the chart is a flat line at 1.0. The curve is a near-vertical line squeezed against the y-axis, and the "n = 63.8" marker sits beside the axis. In round 8 the same chart ran 0–140 and looked great.

      • Hover readings: at 30%, 60% and 95% of the chart width the tooltip reads "n = 525 per group · power 100.0%", "n = 1,173 per group · power 100.0%" and "n = 1,928 per group · power 100.0%".

      • Affected (x-max = 2,000):

        • two-sample t: Sample size and Power

        • paired t: Sample size

        • one-way ANOVA: Power

        • two proportions: Sample size and Power

        • correlation: Sample size and Power

        • chi-square: Sample size and Power

      • Already correct: paired t Power mode (x 0–100) and one-way ANOVA Sample-size mode (x 0–350).

      • Expected: grow the upper bound in small steps (e.g. × 1.25 at a time from the first bound) until power first reaches ≈ 0.99, with no 2,000 floor. Keep a sensible hard cap only for effects so tiny that 0.99 is never reached. The range must never be less than about 1.3 × the solved or current n.

        • Default two-sample: ≈ 0–150 per group.

        • Two proportions .65 vs .45: ≈ 0–230 per group.

        • Correlation r .3: ≈ 0–200.

        • Chi-square w .3, k 3: ≈ 0–250.

        • ANOVA Power mode, N 60, f .25, k 3: ≈ 0–350 total.

    2. Touch: tapping the chart shows nothing, and the page can no longer be scrolled over it on phones.

      • Cause: src/app.js line ≈ 1039 sets the power-curve SVG's touchAction to "none".

      • Tap: with the pointer handlers at lines ≈ 1073–1084, a tap on a 390 px touch viewport still shows no crosshair or tooltip; only mouse hover works.

      • Scroll: because touch-action is none, a vertical swipe that starts on the full-width chart no longer scrolls the page, which traps phone users.

      • Expected: vertical page scrolling still works over the chart (pan-y). A tap, or a horizontal drag, on the chart shows the same crosshair and tooltip as mouse hover (handle pointerdown/pointermove for touch pointers and call the same show-tooltip function). The tooltip stays visible after the finger lifts until the next tap elsewhere.

    3. Regression guard: afterwards the default two-sample result must still be "63.77 per group (continuous) → 64 + 64 (total 128)" with the attrition line "enroll ≈ 71 per group". The Validation panel must still read 32 of 32, and switching through all six tests in all three Solve-for modes must log no console errors.

    12 critic 5.0 min · 5 files changed, 539 insertions(+), 154 deletions(-) Show prompt

    Prompt given to the harness:

    A reviewer tested the app in a browser. Read FEEDBACK.md and fix every item in it, highest priority first. It names the exact functions and line numbers in src/app.js (plus src/body.html and src/app.css for a few items) and gives the expected numbers. Edit those files directly with the file-editing tool; do not explore git history and do not write scratch experiment scripts. Keep everything that already works (see the regression guard at the end of FEEDBACK.md). When done, run python3 build.py --check and make sure it succeeds.
    

    FEEDBACK.md the harness received ✎ written by the AI reviewer:

    Reviewer findings (critic pass): tested index.html in headless Chromium at 1440 px and 390 px

    Read this first. Every item below was reproduced in a browser, and the expected numbers were checked with SciPy. Line numbers refer to the current src/app.js. Open the named functions and edit them directly with the file-editing tool. Do not explore git history, and do not write scratch experiment scripts. Run python3 build.py --check when done. What already works and must not change: the Sample-size and Power results for all six tests match SciPy. The default result is "63.77 per group (continuous) → 64 + 64 (total 128)". Validation shows 32 of 32. Desktop hover works and there are no console errors.
    High priority: wrong numbers or sentences shown to the user
    1. Effect-size mode: the power curve, marker, power table, CSV and methods sentence use the old hidden effect size, not the solved one.

      • Repro: default two-sample t, choose Solve for → Effect size (n 50 + 50, power .80).

        • The Result card correctly shows d = 0.566.

        • The power-curve marker, however, reads "power = 69.7%", below the 80% target line.

        • The table row n = 50 shows 69.7%.

        • The copyable sentence says "(Cohen's d = 0.5 …) requires n = 50 per group … with 80.0% power". That sentence is false: d = 0.5 with 50 per group gives 69.7%.

      • All six tests show this:

        • paired: marker 93.4% vs solved dz 0.404

        • ANOVA: 37.4% vs f 0.411

        • proportions: 52.6% vs h 0.56

        • correlation: 64.7% vs r 0.355

        • chi-square: 53.7% vs w 0.401

      • Cause: these all read state.d / state.dz / state.f / state.w / state.rho1 / hFromProportions(state.p1, state.p2) instead of the solved value res.value:

        • powerAtN() (lines ≈ 590–608), used by renderPowerCurve() (≈ 952–1106) and buildTable() (≈ 1254)

        • renderMethods() (≈ 867–904) and the CSV header in renderTable() (≈ 1280), both via currentEffectValue()

      • Expected: when state.solveFor === "effect_size", the curve, marker, table, CSV # effect_size line and methods sentence all use the solved effect. For example, give powerAtN(n, effectOverride) an optional effect argument.

      • Result for the default case: marker "power = 80.0%" sitting on the target line at n = 50, table row 50 → 80.0%, and a sentence naming d = 0.566. SciPy reference: detectable d = 0.5659.

    2. Methods sentence wording is wrong outside Sample-size mode.

      • In Power mode it says "requires n = 50 per group (total N = 100) to detect the effect with 69.7% power". In Effect size mode it also says "requires".

      • Expected, one phrasing per mode (all six tests):

        • Sample size: keep the current wording.

        • Power: "With n = 50 per group (total N = 100), a two-sided two-sample t test at α = 0.05 has 69.7% power to detect Cohen's d = 0.5 (noncentral t distribution)."

        • Effect size: "With n = 50 per group (total N = 100), a two-sided two-sample t test at α = 0.05 has 80% power to detect an effect of Cohen's d = 0.566 or larger (noncentral t distribution)."

    3. Unequal allocation: the attrition line, methods sentence and CSV ignore group 2.

      • Repro: default two-sample t, set Allocation ratio = 2. The result is correct: "48 + 96 (total 144)".

      • Wrong outputs:

        • The attrition box says "enroll ≈ 54 per group".

        • The methods sentence says "requires n = 48 per group (total N = 144)".

        • The CSV enroll_with_attrition column is computed from n₁ only.

      • Cause: attrition text in renderResults() (≈ 851–856), renderMethods() (≈ 875, 884, 899–900), and renderTable() enroll column (≈ 1297–1303).

      • Expected with ratio ≠ 1, for both two-sample t and two proportions:

        • Attrition: "enroll ≈ 54 + 107 (total 161)", from ceil(47.74 ÷ 0.90) = 54 and ceil(95.48 ÷ 0.90) = 107.

        • Methods: "n₁ = 48 and n₂ = 96 (total N = 144)".

        • CSV: columns n1,n2,power,beta,enroll_n1,enroll_n2.

        • When ratio = 1, keep the current "per group" wording.

    4. Correlation, Effect-size mode with ρ₀ ≠ 0 returns r = 0, and ρ₀ cannot be seen or changed.

      • Repro: Correlation, set H₀ ρ₀ = 0.5 and N = 60, then choose Effect size.

        • The result reads "r = 0", with "Power at rounded design 98.6%".

        • The ρ₀ input disappears, because its whole field group in src/body.html (lines ≈ 59–64) carries data-effinput.

      • Cause: TESTS.correlation.solveEffect() (≈ 448–451) bisects r on [1e-6, 0.999999]. With ρ₀ = 0.5 the power at the lower bound already exceeds the target, so bisect() returns the lower bound.

      • Expected:

        • Keep the ρ₀ input visible in Effect-size mode (hide only the H₁ r input).

        • Solve for the detectable r on (ρ₀, 0.999999).

        • SciPy reference: ρ₀ = .5, N = 60, α .05 two-sided, power .80 → r = 0.726.

        • Validation case R5 (ρ₀ = 0) must still pass.

    Medium priority
    1. Phone: tapping the power curve still shows no tooltip. The cause is now confirmed.

      • On a 390 px touch viewport, a tap fires pointerdown → pointerup → lostpointercapture → pointerout → pointerleave → click.

      • pointerup (line ≈ 1096) sets touchActive = false. Then the pointerleave handler (≈ 1102–1105) calls hide(), so the tooltip vanishes immediately.

      • Expected:

        • pointerleave does nothing for e.pointerType === "touch".

        • After a tap, the crosshair, dot and tooltip ("n = … · power …") stay visible until the next tap.

        • Keep touch-action: pan-y.

    2. Distribution legend numbers disagree with the Result card for small samples.

      • Repro: Paired t, Power mode, 6 pairs, dz 1.2.

        • The Result card says 65.5% power (SciPy 0.6550).

        • The legend says "α = 4.4%" and "β = 36.2% (… power 63.8%)".

      • Cause: in renderDistribution() (≈ 1167–1197):

        • α is summed only over the visible x-range, so the tails are truncated at xLo/xHi.

        • β uses a shifted central t, tCdf(x − ncp), instead of the noncentral t.

      • Expected:

        • The legend α equals the chosen α exactly: use the full tails, nullCdf(−crit) + 1 − nullCdf(crit).

        • β = 1 − (power at the design), computed with nctCdf/ncfCdf/ncx2Cdf over the whole acceptance region.

        • The legend's "power" must match the Result card in every test and mode.

        • The shading may stay clipped to the plot.

    3. Large noncentrality: the alternative (purple) curve becomes a flat line.

      • Repro: ANOVA, Power mode, f 0.4, N 1000 (λ = 160). Also chi-square, w 0.5, N 400 (λ = 100). The alternative density is drawn as 0 everywhere.

      • Cause: the Poisson-mixture loops in ncx2Cdf/ncx2Pdf (≈ 203–230) and ncfCdf (≈ 261–273) start at j = 0 and break as soon as term < 1e-16. For λ/2 ≳ 41 the first weights are already below that, so the loop stops before reaching the bulk of the Poisson mass.

      • Expected:

        • Start at the Poisson mode j = floor(λ/2) and sum outward in both directions, computing weights in log space (logGamma).

        • Stop only when the terms on both sides are negligible.

        • Validation stays at 32 of 32, and the alternative curve is visible for λ up to at least 400.

    Low priority (polish)
    1. The invalid-input state leaves a stale legend. Set Target power to 1.5: the charts clear, but the legend still says "β = 19.5% … power 80.5%". In renderInvalidHint() (≈ 698–709), also clear #dist-legend, and show a short muted message inside each chart card, e.g. "Fix the highlighted inputs to draw this chart".

    2. The power table header shows "B" instead of β. .data-table th { text-transform: uppercase } in src/app.css turns "β" into the capital Greek "Β", which reads as a Latin B, and "n" into "N". Disable uppercase for the power table header, or label the columns "n", "Power", "β (miss rate)" with text-transform: none.

    3. The correlation "Effect size" tile reads "0.3 · r = 0.3". renderResults() (≈ 827) special-cases correlation. Label it with Cohen's r benchmarks (.1/.3/.5) like the other tests: "0.3 · medium".

    4. The phone sticky bar wraps to two lines for ANOVA ("enroll 59 × 3 = 177 total · power 80.5%"). At widths below 480 px, use a shorter text such as "59 × 3 = 177 · 80.5%", or white-space: nowrap with a slightly smaller font, so the bar stays one line.

    Regression guard (check before finishing)
    • The default page still shows "63.77 per group (continuous) → 64 + 64 (total 128)", "enroll ≈ 71 per group", and Validation "32 of 32 cases within tolerance".

    • Switching through all six tests × three Solve-for modes logs no console errors.

    • The power-curve x-axis ranges stay as they are now (default two-sample: about 0–170).

    13 critic 0.8 min · 4 files changed, 103 insertions(+), 154 deletions(-) Show prompt

    Prompt given to the harness:

    A reviewer re-tested the app in a browser. Read FEEDBACK.md and fix the three small items in it. It names the exact functions and line numbers in src/app.js and src/body.html and gives the expected output. Edit those files directly with the file-editing tool; do not explore git history and do not write scratch experiment scripts. Change nothing else (see the regression guard at the end of FEEDBACK.md). When done, run python3 build.py --check and make sure it succeeds.
    

    FEEDBACK.md the harness received ✎ written by the AI reviewer:

    Reviewer findings (critic pass 2): tested index.html in headless Chromium at 1440 px and 390 px

    Read this first. The previous round fixed every earlier item, and I re-checked them against SciPy: - Effect-size mode now uses the solved effect. - Correlation with ρ₀ = .5 gives r = 0.726. - Unequal attrition shows 54 + 107. - The legend matches the Result card. - A phone tap shows the tooltip. - Large-λ curves are drawn. - The table header reads β, and the sticky bar is one line. Validation is 32 of 32, with no console errors. Only the three small items below remain. Edit src/app.js (and src/body.html for item 3) directly with the file-editing tool. Do not explore git history and do not write scratch experiment scripts. Change nothing else, then run python3 build.py --check.
    1. The CSV is misaligned for unequal allocation, because the n₂ value is missing from every row.

      • Repro: default two-sample t, set Allocation ratio = 2, click Download CSV.

        • The header row is n1,n2,power,beta,enroll_n1,enroll_n2 (6 columns).

        • Every data row has only 5 values, e.g. 23,0.488016,0.511984,26,52, so power lands under n2.

      • Cause: renderTable(), line ≈ 1411, builds each row as n,power,beta,enroll(n), and never writes n₂.

      • Expected for ratio ≠ 1 (two-sample t and two proportions): 23,46,0.488016,0.511984,26,52, where n₂ = ceil(n₁ × ratio).

      • On-screen table too: when ratio ≠ 1, add an "n₂" column after "n" and label the first column "n₁", so the visible table matches the CSV. Keep the ratio = 1 and ANOVA formats exactly as they are now.

    2. Attrition rounding is inconsistent between equal and unequal allocation.

      • Equal groups inflate the continuous n: 63.77 ÷ 0.90 → enroll 71.

      • Unequal groups inflate the already rounded n (renderResults() ≈ 893–898 and renderMethods() unequal branch, using d.n1 / d.n2). This gives a different answer.

        • Repro: Two proportions, p₁ 0.3, p₂ 0.2, ratio 1.5.

        • It shows "enroll ≈ 272 + 406" (ceil(244 ÷ 0.90) = 272).

        • The continuous rule used everywhere else gives 271 + 406: ceil(243.07 ÷ 0.90) = 271 and ceil(364.61 ÷ 0.90) = 406.

      • Expected: in both places, use the continuous solution for each group, n₁ = res.value and n₂ = res.value × state.ratio.

        • Text: "enroll ≈ 271 + 406 (total 677) (ceil(243.07 ÷ 0.90) and ceil(364.61 ÷ 0.90))".

        • Default ratio-2 case: must still show 54 + 107 (total 161).

    3. The power-curve subtitle says "hover for values" (src/body.html line ≈ 166). Phones use tap now, so change it to "hover or tap for values".

    Regression guard (check before finishing)
    • The default page shows "63.77 per group (continuous) → 64 + 64 (total 128)", "enroll ≈ 71 per group", and "32 of 32 cases within tolerance".

    • Effect-size mode (default two-sample t) still shows d = 0.566 with the curve marker "power = 80.0%".

    • No console errors in any of the 18 test × Solve-for combinations.

What the agent was asked

The full specification the harness received, rendered from the original Markdown.

Read the specification

Study Power Lab — Sample size and statistical power, visually

Category: Research Who it is for: Graduate students, IRB/IACUC protocol writers, faculty planning experiments

What it should do

Figure out how many participants, plots or animals your study needs — and see why — with interactive power curves for the tests researchers actually use.

Data

No external data; implement the statistics in JavaScript (normal and noncentral t / F / chi-square approximations or numeric integration), and validate a handful of results against published reference values (e.g. Cohen 1988 tables or G*Power outputs) listed in the app's 'validation' panel.

Raw downloads (if any) are in data/raw/ with data/raw/SOURCES.md describing where each file came from. Transform what the app needs into compact JSON in data/embed/ (write a small script in scripts/ to do it, so the transformation is reproducible). Every dataset the app shows must be credited in a visible "Data & sources" section of the app, with links. If a dataset is illustrative/synthetic, label it clearly as such in the UI — never present made-up numbers as real University of Idaho figures.

Required features

  • Test chooser: two-sample t, paired t, one-way ANOVA, two proportions, correlation, chi-square goodness-of-fit

  • Solve for any one of: sample size, power, detectable effect size — given alpha and the others

  • Live power curve (power vs n) and a distribution plot showing null vs alternative with α and β regions shaded

  • Effect-size helpers (Cohen's d from means/SD, f from group means, h from proportions) with plain-language small/medium/large guidance

  • Attrition/dropout inflation and unequal allocation ratio

  • Copyable methods-section sentence and CSV of the power table

Quality bar

This app is going into a University of Idaho gallery that shows faculty, staff and students what they can build with AI coding agents. It must look polished and modern (think a well-designed data product, not a homework demo), feel alive (smooth transitions, responsive hover/selection states, animated where it helps understanding), and do real, useful work — every control should change something meaningful. It must work well on a laptop and on a phone. Include a short "How to use" hint for first-time visitors and a helpful empty state wherever the user must supply data.

Known limitations

Reported by the independent critic. ✎ written by the AI critic

  • Charts are rebuilt from scratch on every input, so the CSS transitions on the curve and marker never animate. The page updates instantly but does not animate between states.
  • Data caveat inherited from data prep: the Cohen h and w benchmarks were not checked against a retrieved copy of Cohen (1988/1992). The two-sample t, arcsine two-proportion and chi-square goodness-of-fit validation cases rely on statsmodels/SciPy agreement only, because the G*Power manual has no worked examples for them.
  • Correlation uses the Fisher z approximation. The UI labels this, but G*Power's exact method can differ by one participant (manual example: exact 1928 vs approximation 1929).
  • Two-proportions Effect-size mode reports the detectable Cohen's h only, not the corresponding detectable p2.
  • On phones a tapped tooltip stays until the next tap on the chart. Tapping elsewhere does not dismiss it.
  • The harness left two near-duplicate scripts, scripts/smoke_test.js and scripts/smoke-test.js. They are not part of the page.
  • Harness independence: the critic rounds (12-13), like the driver's later rounds, needed feedback that named functions, line numbers and expected values. With that guidance ForgeCode fixed all 11 critic items in round 12 and the remaining 3 in round 13 without regressions.