1. MicroPython engine (the block as shipped) vs SciPy
     k=2  df=1   a=0.050  python 17.96929  scipy 17.96929
     k=3  df=12  a=0.050  python 3.77293  scipy 3.77293
     k=10 df=2   a=0.010  python 31.68935  scipy 31.68935
  ok   python q crit, max relative error < 1E-5 (5.04e-06)
  ok   python p at the true q crit, max relative error < 1E-4 (2.42e-05)
  ok   Lessons s11 case k=4 df=3 a=.005 -> 15.4503 (15.4503)
  ok   df = infinity (100000) matches SciPy df=inf-ish (1e6) (0.0355570 vs 0.0355570)
  ok   Diagnostics probe value F(3.5;3,12) within 1E-5 of 0.9300045147 (0.9300045147)
2. PPL engine (mirror) vs SciPy, the menu resolution (64 x 32/48)
  ok   PPL q crit, max absolute error < 1E-2 (df >= 3; it is k = 20, df = 3 at the worst) (8.1e-03 at k=20 df=3 a=0.005)
  ok   sr_LGAM (Lanczos) vs lgamma < 1E-12 (1.8e-15)
3. The plot cache (32 x 48) and spdf vs SciPy
  ok   cache CDF max abs error < 5E-4 (curve level; worst k = 20, df = 2) (3.0e-04)
  ok   cache pdf max error relative to the peak < 1E-2 (curve level) (5.7e-03)
  ok   cache(): 65 + 65 + 1 = 131 crossings (131)
  ok   cache(): CDF monotone, last element in [0,1] (F(qmax) = 0.99981, qmax = 8.400)
4. The H0 simulation (the block's simulate(), urandom -> random.random)
  ok   k=3 m=5 a=0.05: q > q crit 5.19 % (alpha 5 %, |z| < 4) (z = 0.87)
  ok   k=3 m=5: any t > t crit 11.56 % vs theory 11.56 % (|z| < 4) (z = 0.01)
  ok   k=4 m=8 a=0.05: q > q crit 5.17 % (alpha 5 %, |z| < 4) (z = 0.78)
  ok   k=4 m=8: any t > t crit 19.64 % vs theory 19.49 % (|z| < 4) (z = 0.37)
  ok   k=6 m=4 a=0.10: q > q crit 10.10 % (alpha 10 %, |z| < 4) (z = 0.33)
  ok   k=6 m=4: any t > t crit 53.13 % vs theory 52.85 % (|z| < 4) (z = 0.56)
  ok   k=3 m=5 a=0.01: q > q crit 1.00 % (alpha 1 %, |z| < 4) (z = 0.00)
  ok   k=3 m=5: any t > t crit 2.49 % vs theory 2.51 % (|z| < 4) (z = -0.15)
     (theory for the family-wise line: any pair exceeds t crit exactly when the
      largest pair does, i.e. when q > sqrt2 * t crit)
5. k = 2: q = t * sqrt 2
  ok   q crit(k=2) = sqrt2 * t crit (two-sided), SciPy and the Python engine (1.8e-08)
6. The real PPL source in the harness (v3d)
  ok   harness sr_SRCDF(3.5,3,12) = 0.930005 (SciPy 0.930005) 
  ok   harness sr_SRCDF(4,5,20) = 0.930413 (SciPy 0.930413) 
  ok   harness sr_SRCDF(5,4,3) = 0.890110 (SciPy 0.890109) 
  ok   harness sr_SRCrit(.05,3,12) = 3.77293 (SciPy 3.77293) 
  ok   P-value via Python: EP_QPVAL 0.069995 (SciPy 0.069995), badge shown, SR_ENGINE Python 
  ok   P-value via PPL: EP_QPVAL 0.069995 (SciPy 0.069995), badge shown, SR_ENGINE PPL 
  ok   Critical value df=2 via Python: 8.3308 (SciPy 8.3308), no MSGBOX 
  ok   Critical value df=2 via PPL: raised to df 3 with the beta 14 message (5.9096, SciPy df3 5.9096) 
     result: StudRange: q crit (k = 3, ν = 3, α = 0.0500) = 5.9096, p not computed, HSD not computed (engine: PPL)
  ok   HSD = 1.3652 (SciPy 1.3652) 
  ok   yardstick verdict for means 10 and 12.5 says significant (|diff| 2.5 > 1.365) ['|\ue0001 − \ue0003| = 2.500 > HSD: significant']
  ok   plot via Python: badge on the plot 
     header readouts seen: ['α = 0.050   q crit = 3.773', 'α = 0.100   q crit = 3.204', 'α = 0.110   q crit = 3.123', 'α = 0.144   q crit = 2.891']
     q obs readouts (last 3): ['q obs = 3.204   p = 0.1000', 'q obs = 3.204   p = 0.1000', 'q obs = 3.204   p = 0.1000']
  ok   soft keys drawn 
  ok   after the q crit tap: q obs = q crit and p = alpha (α = 0.100   q crit = 3.204 | q obs = 3.204   p = 0.1000)
  ok   plot via PPL: red badge and a 6-item plot menu without the Python extras 
  ok   simulation ran through the bridge (2,000 replicates; q > q crit 4.5 %, any t 11.7 %) 
     page/panel lines: ['Of 2,000 replicates, 4.5 % exceeded', 'any t > t crit: 11.7 %', 'q > q crit: 4.5 %']
  ok   k = 2 curve arrived and its legend is drawn 
  ok   Diagnostics with Python answering: ['Python bridge OK (1 ms)'] 
  ok   Diagnostics with Python failing: ['Python bridge NOT answering'] 
  ok   help: 4 pages, no line estimated clipped []
7. beta 15a: the plot soft keys under WAIT(-1), stale clicks, the Quit summary (v3d)
  ok   7a soft key 1 (α−) on a type-3 click: α 0.050 -> 0.025 [0.025, 0.05]
  ok   7a soft key 2 (α+) on a type-3 click: α 0.050 -> 0.100 [0.05, 0.1]
  ok   7a soft key 3 (k−): k 3 -> 2 k = 2   ν = 12
  ok   7a soft key 4 (k+): k 3 -> 4 k = 4   ν = 12
  ok   7a soft key 5 (df): the df form opens; df 20 rebuilds the plot k = 3   ν = 20
  ok   7a soft key 6 (Menu): the plot menu opens ['StudRange — Tukey q', 'Plot menu', 'StudRange — Tukey q']
  ok   7b beta 15 in v3d: α+ and Menu do nothing (the emulator's FAIL) [0.05]
  ok   7c a type-0 PRESS on the strip (the v3b model) does nothing -- neutralised [0.05]
  ok   7c a firmware that sent {0}, {2} AND {3} for a strip tap would still step α ONCE (0.05 -> 0.10) [0.05, 0.05, 0.05, 0.1, 0.1]
  ok   7d a tap in the plot ({0}, {2}, {3}) moves q obs to the finger (4.200): q obs = 4.200   p = 0.0293 
  ok   7d a tap on the q crit box sets q obs = q crit (α = 0.050   q crit = 3.773 | q obs = 3.773   p = 0.0500) 
  ok   7e the plot's help page closed by a footer tap (x 150, y 226): no soft key fires on the plot (k stays 3, α stays 0.05) [0.05] ['k = 3   ν = 12']
  ok   7e the plot's help page closed by a footer tap (x 150, y 226) -- mouse_tail worst case: no soft key fires on the plot (k stays 3, α stays 0.05) [0.05] ['k = 3   ν = 12']
  ok   7e (without the guard, in that worst case, the click WOULD run soft key 3: k 3 -> 2) k = 2   ν = 12
  ok   7f Quit with nothing computed: "StudRange: q crit not computed, p not computed, HSD not computed (engine: none)" 
  ok   7f after HSD only (the emulator's run): "StudRange: q crit (k = 4, ν = 28, α = 0.0500) = 3.8612, p not computed, HSD = 1.3652 (engine: Python)" want StudRange: q crit (k = 4, ν = 28, α = 0.0500) = 3.8612, p not computed, HSD = 1.3652 (engine: Python)
  ok   7f (beta 15 in the same run: "StudRange: q crit = 3.8612, p = 0.00000, HSD = 1.3652 (engine: Python)" -- the emulator's "p = 0.00000") 
  ok   7f after P-value and Critical value: "StudRange: q crit (k = 3, ν = 12, α = 0.0500) = 3.7729, p = 0.07000, HSD not computed (engine: Python)" want StudRange: q crit (k = 3, ν = 12, α = 0.0500) = 3.7729, p = 0.07000, HSD not computed (engine: Python)
  ok   7f the PPL fallback raised df 2 to 3: the summary names the df it used, ν = 3 ("StudRange: q crit (k = 3, ν = 3, α = 0.0500) = 5.9096, p not computed, HSD not computed (engine: PPL)") 
  ok   7f df 0 (infinity) is named with the infinity sign ("StudRange: q crit (k = 3, ν = ∞, α = 0.0500) = 3.3145, p not computed, HSD not computed (engine: Python)") 
  ok   7g routines that differ from beta 15: ['StudRange', 'sr_CritMode', 'sr_HSDMode', 'sr_PlotMode', 'sr_PvMode', 'sr_WaitAny'] want ['StudRange', 'sr_CritMode', 'sr_HSDMode', 'sr_PlotMode', 'sr_PvMode', 'sr_WaitAny']
  ok   7g the #PYTHON srq block is byte-identical to beta 15 
  ok   7g 15b (and 15c: + sr_Eq; 15d: + sr_BuildZ, sr_Cache, sr_HelpMode): routines that differ from beta 15a: ['StudRange', 'sr_BuildZ', 'sr_Cache', 'sr_CritMode', 'sr_Eq', 'sr_FmtF', 'sr_HSDMode', 'sr_HelpMode', 'sr_PlotMode', 'sr_PvMode', 'sr_SimRun', 'sr_Yard'] want ['StudRange', 'sr_BuildZ', 'sr_Cache', 'sr_CritMode', 'sr_Eq', 'sr_FmtF', 'sr_HSDMode', 'sr_HelpMode', 'sr_PlotMode', 'sr_PvMode', 'sr_SimRun', 'sr_Yard']
  ok   7g 15b: sr_WaitAny (it still appends its tap) is byte-identical to beta 15a 
  ok   7g 15b: the #PYTHON srq block is byte-identical to beta 15a (and to beta 15) 
  ok   7g 15b: outside those routines only the new file-level lines differ (LOCAL srQS:="";, LOCAL srAS:="";, LOCAL srPS:="";, LOCAL srQueuedMs:=100;, sr_Eq();); the rest is comments 
  ok   7g 15b: the stale test reads the threshold by name, set in ONE place; the clock is read around the wait 
  ok   7g 15b: the type-3 soft-key logic the emulator confirmed is unchanged 
8. beta 15b: the stale spot, the Quit summary, small numbers (v3d)
   8a. the stale spot, both ways
  ok   8a beta 15a, the emulator's case: Help closed on its footer at the α− key, then α− once -> nothing (α stays 0.050) [0.05]
  ok   8a beta 15a: ... a second α− tap works (the emulator: "ignored once, then works"): 0.050 -> 0.025 [0.025, 0.05]
  ok   8a beta 15b: the same taps -- α− works on the FIRST tap (a real, later tap at the spot counts): 0.050 -> 0.025 [0.025, 0.05]
  ok   8a beta 15b: a click ALREADY QUEUED at the α− spot (mouse_tail, a clock that has not moved (plain v3d)) is still skipped: α stays 0.050 [0.05]
  ok   8a beta 15b: a click ALREADY QUEUED at the α− spot (mouse_tail, the student's taps 400 ms apart) is still skipped: α stays 0.050 [0.05]
  ok   8a beta 15b: ... and the real α− tap after that queued click counts, ONCE: 0.050 -> 0.025 [0.025, 0.05]
  ok   8a (with the stale test patched out, that queued click WOULD step α: 0.050 -> 0.025 -- the skip is real) [0.025, 0.05]
  ok   8a beta 15b, time passing: a tap in the plot still moves q obs to the finger once (4.200) q obs = 4.200   p = 0.0293
  ok   8a beta 15b, time passing: two α+ taps step α twice (0.050 -> 0.100 -> ... ) -- soft keys unchanged [0.05, 0.1]
   8b. the Quit summary reports what the student saw
  ok   8b plot only (the emulator's session): "StudRange: q crit (k = 3, ν = 12, α = 0.050) = 3.773, p = 0.0700, HSD not computed (engine: Python)" -- the plot's own readouts "α = 0.050   q crit = 3.773" / "q obs = 3.500   p = 0.0700" want StudRange: q crit (k = 3, ν = 12, α = 0.050) = 3.773, p = 0.0700, HSD not computed (engine: Python)
  ok   8b ... and those are SciPy's q crit and p to the plot's decimals (3.773 / 0.0700; curve-level accuracy) 
  ok   8b ... EP_QCRIT and EP_QPVAL are still not written by the plot (beta 14) 0.0 0.0
  ok   8b (beta 15a, same session: "StudRange: q crit not computed, p not computed, HSD not computed (engine: Python)" -- the emulator's finding) 
  ok   8b plot, α− then k+: the summary has k = 4, α = 0.025, q crit = 4.761, as on the last frame ("StudRange: q crit (k = 4, ν = 12, α = 0.025) = 4.761, p = 0.1151, HSD not computed (engine: Python)") k = 4   ν = 12
  ok   8b plot with no q obs: "StudRange: q crit (k = 3, ν = 12, α = 0.050) = 3.773, p not computed, HSD not computed (engine: Python)" (p not computed) 
  ok   8b α = 0.001 on the plot: the summary says what the header said ("α = 0.001   q crit = 6.917" / "StudRange: q crit (k = 3, ν = 12, α = 0.001) = 6.917, p = 0.0700, HSD not computed (engine: Python)") 
  ok   8b P-value, then the plot (k 4, df 20, q obs 4.5): the plot's p is the last seen ("StudRange: q crit (k = 4, ν = 20, α = 0.050) = 3.958, p = 0.0223, HSD not computed (engine: Python)") 
  ok   8b the plot, then P-value: the P-value page's p (5 decimals) is the last seen ("StudRange: q crit (k = 4, ν = 20, α = 0.050) = 3.958, p = 0.07000, HSD not computed (engine: Python)") 
  ok   8b a P-value below 0.00001 reads "p < 0.00001" in the summary ("StudRange: q crit not computed, p < 0.00001, HSD not computed (engine: Python)") 
   8c. no fixed decimals in scientific form
  ok   8c sr_FmtF(0.0004, 3) = "0.000" want 0.000
  ok   8c sr_FmtF(0.096, 1) = "0.1" want 0.1
  ok   8c sr_FmtF(-1e-17, 1) = "0.0" want 0.0
  ok   8c sr_FmtF(0.05, 1) = "0.1" want 0.1
  ok   8c sr_FmtF(3.77293, 3) = "3.773" want 3.773
  ok   8c sr_FmtF(1e-06, 4) = "0.0000" want 0.0000
  ok   8c sr_FmtF(-2.5, 2) = "-2.50" want -2.50
  ok   8c ... none of them printed in scientific form []
     (beta 15a on the same run: ['-2.8ᴇ-17', '1.0000ᴇ-10', '1.3652ᴇ-5', '2.75700ᴇ-7', '4.000ᴇ-4'])
  ok   8c yardstick: means -0.3, 0.0004, 0.2, 0.25 print as ['\ue0001 = -0.300', '\ue0002 = 0.000', '\ue0003 = 0.200', '\ue0004 = 0.250']; the ticks ['-0.3', '-0.2', '-0.1', '0.0', '0.1', '0.2'] -- no scientific form, no "-0.0" []
  ok   8c F(q) for q = 0.001 (a probability under 0.00001) reads "F(q) < 0.00001" (15c: no "=" before the relation) 
  ok   8c a tiny MSE gives HSD "0.0000" on the page and in the summary ("StudRange: q crit (k = 4, ν = 28, α = 0.0500) = 3.8612, p = 1.00000, HSD = 0.0000 (engine: Python)") 
  ok   8c one replicate in 2,000 = 0.05 % prints "0.1" (STRING(0.05, 2, 1) is "5.0ᴇ-2") 
   8d. INSTRING, LEFT, MID
  ok   8d the PPL code (outside comments, strings and the #PYTHON block) calls none of INSTRING, RIGHT, MID, and LEFT only twice, in sr_Eq, inside IF DIM(eqS)>0 ['LEFT', 'LEFT']
9. beta 15c: no "=" before a relation, no "0.00000" for a p that is not 0 (v3e)
  ok   9a 15b, q 3.5, k 3, df 12: ['P(Q ≥ q) = 0.07000', 'F(q) = 0.93000'], summary "p = 0.07000" (['P(Q ≥ q) = 0.07000', 'F(q) = 0.93000'], 'p = 0.07000', 0.06999547724511279)
  ok   9a q 3.5, k 3, df 12: an ordinary page and summary are the same in 15b and 15c: ['P(Q ≥ q) = 0.07000', 'F(q) = 0.93000'], "p = 0.07000" ((['P(Q ≥ q) = 0.07000', 'F(q) = 0.93000'], 'p = 0.07000', 0.06999547724511279), (['P(Q ≥ q) = 0.07000', 'F(q) = 0.93000'], 'p = 0.07000', 0.06999547724511279))
  ok   9a q 3.5: EP_QPVAL is the computed p, the same in 15b and 15c (0.07) 
  ok   9a 15b, q 12, k 3, df 12: ['P(Q ≥ q) = < 0.00001', 'F(q) = 0.99999'], summary "p < 0.00001" (['P(Q ≥ q) = < 0.00001', 'F(q) = 0.99999'], 'p < 0.00001', 5.6838621038668435e-06)
  ok   9a 15c, q 12, k 3, df 12: ['P(Q ≥ q) < 0.00001', 'F(q) = 0.99999'], summary "p < 0.00001" (['P(Q ≥ q) < 0.00001', 'F(q) = 0.99999'], 'p < 0.00001', 5.6838621038668435e-06)
  ok   9a q 12: EP_QPVAL is the computed p, the same in 15b and 15c (5.68e-06) 
  ok   9a 15b, q 40, k 3, df 12: ['P(Q ≥ q) = 0.00000', 'F(q) = 1.00000'], summary "p = 0.00000" (['P(Q ≥ q) = 0.00000', 'F(q) = 1.00000'], 'p = 0.00000', 0.0)
  ok   9a 15c, q 40, k 3, df 12: ['P(Q ≥ q) < 0.00001', 'F(q) = 1.00000'], summary "p < 0.00001" (['P(Q ≥ q) < 0.00001', 'F(q) = 1.00000'], 'p < 0.00001', 0.0)
  ok   9a q 40: EP_QPVAL is the computed p, the same in 15b and 15c (0) 
  ok   9a 15b, q 0.001, k 3, df 12: ['P(Q ≥ q) = 1.00000', 'F(q) = < 0.00001'], summary "p = 1.00000" (['P(Q ≥ q) = 1.00000', 'F(q) = < 0.00001'], 'p = 1.00000', 0.9999997243356662)
  ok   9a 15c, q 0.001, k 3, df 12: ['P(Q ≥ q) = 1.00000', 'F(q) < 0.00001'], summary "p = 1.00000" (['P(Q ≥ q) = 1.00000', 'F(q) < 0.00001'], 'p = 1.00000', 0.9999997243356662)
  ok   9a q 0.001: EP_QPVAL is the computed p, the same in 15b and 15c (1) 
  ok   9a 15b, q 0, k 3, df 12: ['P(Q ≥ q) = 1.00000', 'F(q) = 0.00000'], summary "p = 1.00000" (['P(Q ≥ q) = 1.00000', 'F(q) = 0.00000'], 'p = 1.00000', 1.0)
  ok   9a q 0, k 3, df 12: an ordinary page and summary are the same in 15b and 15c: ['P(Q ≥ q) = 1.00000', 'F(q) = 0.00000'], "p = 1.00000" ((['P(Q ≥ q) = 1.00000', 'F(q) = 0.00000'], 'p = 1.00000', 1.0), (['P(Q ≥ q) = 1.00000', 'F(q) = 0.00000'], 'p = 1.00000', 1.0))
  ok   9a q 0: EP_QPVAL is the computed p, the same in 15b and 15c (1) 
  ok   9a q 5, k 4, df 20: an ordinary page and summary are the same in 15b and 15c: ['P(Q ≥ q) = 0.01029', 'F(q) = 0.98971'], "p = 0.01029" ((['P(Q ≥ q) = 0.01029', 'F(q) = 0.98971'], 'p = 0.01029', 0.01028723575575341), (['P(Q ≥ q) = 0.01029', 'F(q) = 0.98971'], 'p = 0.01029', 0.01028723575575341))
  ok   9a q 5: EP_QPVAL is the computed p, the same in 15b and 15c (0.0103) 
  ok   9a q 8, k 6, df 30: an ordinary page and summary are the same in 15b and 15c: ['P(Q ≥ q) = 0.00005', 'F(q) = 0.99995'], "p = 0.00005" ((['P(Q ≥ q) = 0.00005', 'F(q) = 0.99995'], 'p = 0.00005', 5.010280772155262e-05), (['P(Q ≥ q) = 0.00005', 'F(q) = 0.99995'], 'p = 0.00005', 5.010280772155262e-05))
  ok   9a q 8: EP_QPVAL is the computed p, the same in 15b and 15c (5.01e-05) 
  ok   9a q 40: the PPL engine's p is exactly 0 (1 - F, F reached 1; SciPy 6.7e-12), shown "< 0.00001" 
  ok   9a the Quit summary after q 40: "StudRange: q crit not computed, p < 0.00001, HSD not computed (engine: PPL)" 
  ok   9a the Quit summary after q 3.5: "StudRange: q crit not computed, p = 0.07000, HSD not computed (engine: PPL)" 
  ok   9a the Quit summary after q 12: "StudRange: q crit not computed, p < 0.00001, HSD not computed (engine: PPL)" 
  ok   9b sr_Eq: "< 0.00001" -> " < 0.00001", "0.07000" -> " = 0.07000", "" -> " = " (no error): [' < 0.00001', ' = 0.07000', ' = ', ' = 1.00000'] 
  ok   9c routines that differ from beta 15b: ['sr_Eq', 'sr_PvMode'] (sr_Eq new, sr_PvMode) 
  ok   9c the #PYTHON srq block is byte-identical to beta 15b 
  ok   9c the code lines that changed are exactly the listed ones: -[] +[] 
  ok   9d the new names sr_Eq, eqS, pvS are not built-ins (case ignored); sr_Eq carries the sr_ tag 
  ok   9d version strings (15d: beta 15d's): both banners, the tested-on lines, the "What changed in beta 15d" paragraph above beta 15c's 
  ok   9d no // comment inside an open bracket: [] 
10. beta 15d: S3, S1, Home's X, the disclaimer (v3e)
   10a. S3: spaces round "=" on the HSD page
  ok   10a HSD page (Python answering), first line: 15c "α=0.0500  k=4  m=8  ν=28" -> 15d "α = 0.0500  k = 4  m = 8  ν = 28"; the other lines unchanged 
  ok   10a "α = 0.0500  k = 4  m = 8  ν = 28": 32 characters, ends at x 252 at 7.5 px a character (the harness) or 300 at 9 (the review's cautious estimate), inside 320 
  ok   10a HSD page (Python answering), first line: 15c "α=0.0100  k=20  m=100  ν=1980" -> 15d "α = 0.0100  k = 20  m = 100  ν = 1980"; the other lines unchanged 
  ok   10a "α = 0.0100  k = 20  m = 100  ν = 1980": 37 characters, ends at x 289 at 7.5 px a character (the harness) or 345 at 9 (the review's cautious estimate), inside 320 
  ok   10a no display label is glued to its "=" any more (a string ending "x=" before a value): [] 
   10b. S1: the note on the Critical value and HSD pages, PPL engine below alpha 0.005
  ok   10b Critical value, alpha 0.0001, k 20, df 3, PPL engine: the two note lines replace the two blank ones ['α = 0.0001   k = 20   ν = 3', '', 'q crit = 94.6581', 'PPL fallback at α < 0.005:', 'q crit may be off by a few %', 'Reject H0 for a pair if q ≥ q crit', '(matches Harter 1959 tables)', 'Stored in EP_QCRIT']
  ok   10b Critical value, alpha 0.0049, k 3, df 12, PPL engine: the two note lines replace the two blank ones ['α = 0.0049   k = 3   ν = 12', '', 'q crit = 5.6122', 'PPL fallback at α < 0.005:', 'q crit may be off by a few %', 'Reject H0 for a pair if q ≥ q crit', '(matches Harter 1959 tables)', 'Stored in EP_QCRIT']
  ok   10b Critical value, alpha 0.005, k 3, df 12, PPL engine: the page is beta 15c's ['α = 0.0050   k = 3   ν = 12', '', 'q crit = 5.5960', '', 'Reject H0 for a pair if q ≥ q crit', '(matches Harter 1959 tables)', '', 'Stored in EP_QCRIT']
  ok   10b Critical value, alpha 0.05, k 3, df 12, PPL engine: the page is beta 15c's ['α = 0.0500   k = 3   ν = 12', '', 'q crit = 3.7729', '', 'Reject H0 for a pair if q ≥ q crit', '(matches Harter 1959 tables)', '', 'Stored in EP_QCRIT']
  ok   10b Critical value, alpha 0.0001, k 20, df 3, Python: the page is beta 15c's ['α = 0.0001   k = 20   ν = 3', '', 'q crit = 92.9443', '', 'Reject H0 for a pair if q ≥ q crit', '(matches Harter 1959 tables)', '', 'Stored in EP_QCRIT']
  ok   10b Critical value, alpha 0.001, k 4, df 3, Python: the page is beta 15c's ['α = 0.0010   k = 4   ν = 3', '', 'q crit = 26.6427', '', 'Reject H0 for a pair if q ≥ q crit', '(matches Harter 1959 tables)', '', 'Stored in EP_QCRIT']
  ok   10b why: k 20, df 3, alpha 1E-4: the PPL engine gives 94.658, Python 92.944, SciPy 92.944 (PPL 1.8 % off; the number is unchanged in 15d, only said) 
  ok   10b Tukey HSD, alpha 0.001, k 4, m 8, df 3, PPL engine: the two note lines replace the two blank ones ['α = 0.0010  k = 4  m = 8  ν = 3', 'MSE = 1.0000', 'q crit = 26.6402', 'HSD = q·√(MSE/m) = 9.4187', 'PPL fallback at α < 0.005:', 'q crit, HSD may be off a few %', '|\ue000i - \ue000j| > HSD → significant', 'Stored in EP_QCRIT, EP_HSD']
  ok   10b Tukey HSD, alpha 0.05, k 4, m 8, df 3, PPL engine: the page is beta 15c's (but S3) ['α = 0.0500  k = 4  m = 8  ν = 3', 'MSE = 1.0000', '', 'q crit = 6.8245', 'HSD = q·√(MSE/m) = 2.4128', '', '|\ue000i - \ue000j| > HSD → significant', 'Stored in EP_QCRIT, EP_HSD']
  ok   10b Tukey HSD, alpha 0.001, k 4, m 8, df 3, Python: the page is beta 15c's (but S3) ['α = 0.0010  k = 4  m = 8  ν = 3', 'MSE = 1.0000', '', 'q crit = 26.6427', 'HSD = q·√(MSE/m) = 9.4196', '', '|\ue000i - \ue000j| > HSD → significant', 'Stored in EP_QCRIT, EP_HSD']
  ok   10b the note lines are 30 characters at most (font 3: under the review's 34) 
   10c. Home's X is no longer the MAKELIST counter
  ok   10c beta 15c counts with Home's X in 5 MAKELIST calls (sr_BuildZ 3, sr_Cache 2); the harness leaves X at 7.25 there (it does not model the counter write, a device fact: LL section 5) 
  ok   10c beta 15d: Home's X is still 7.25 after both, and the numbers are beta 15c's (q crit 3.7729; the node table; the 65-point CDF and pdf cache and its window) 
  ok   10c no MAKELIST in beta 15d counts with X; the new counters bzI, cbI are LOCAL 
   10d. the disclaimer (Roger, 19 Oct 2026)
  ok   10d the header carries the disclaimer word for word (15c: no) 
  ok   10d About page 1/4: beta 15d adds "Teaching aid; not for legal use." as its ninth line (32 characters), so its lines are 18 px apart (30 ... 174) instead of 22; the eight lines before it are beta 15c's [('After Dunlap et al. 1977, BRMI 9(4)', 156.0), ('Teaching aid; not for legal use.', 174.0)]
   10e. the routine diff and the line diff, 15c -> 15d
  ok   10e routines that differ from beta 15c: ['sr_BuildZ', 'sr_Cache', 'sr_CritMode', 'sr_HSDMode', 'sr_HelpMode'] 
  ok   10e the #PYTHON srq block is byte-identical to beta 15c 
  ok   10e outside those routines only comments differ 
  ok   10e the other 26 routines are byte-identical, comments included 
     code lines removed: 17, added: 41
       - LOCAL bzJ,bzH,zv,bzW;
       - gZN:=MAKELIST(0,X,1,nz+1);
       - gZW:=MAKELIST(0,X,1,nz+1);
       - gZC:=MAKELIST(0,X,1,nz+1);
       - cdf:=MAKELIST(0,X,1,nn+1);
       - pdf:=MAKELIST(0,X,1,nn+1);
       - LOCAL ta,tk,tv,qc;
       - sr_ShowRes("Critical value q(α)",
       - {"α = "+sr_FmtF(ta,4)+"   k = "+STRING(tk,2,0)+"   ν = "+IFTE(tv==0,CHAR(8734),STRING(tv,2,0)),
       - "Stored in EP_QCRIT"},
       - "any key or tap = menu",1);
       - LOCAL qc,hsd,xb,hk;
       - hk:=sr_ShowRes("Tukey HSD",
       - {"α="+sr_FmtF(ta,4)+"  k="+STRING(tk,2,0)+"  m="+STRING(tm,2,0)+"  ν="+STRING(vv,2,0),
       - "Stored in EP_QCRIT, EP_HSD"},
       - "Ent or tap = yardstick, Esc = menu",1);
       - "After Dunlap et al. 1977, BRMI 9(4)"},
       + LOCAL bzJ,bzH,zv,bzW,bzI;
       + gZN:=MAKELIST(0,bzI,1,nz+1);
       + gZW:=MAKELIST(0,bzI,1,nz+1);
       + gZC:=MAKELIST(0,bzI,1,nz+1);
       + LOCAL cbI;
       + cdf:=MAKELIST(0,cbI,1,nn+1);
       + pdf:=MAKELIST(0,cbI,1,nn+1);
       + LOCAL ta,tk,tv,qc,crH,crL;
       + crH:="α = "+sr_FmtF(ta,4)+"   k = "+STRING(tk,2,0)+"   ν = "+IFTE(tv==0,CHAR(8734),STRING(tv,2,0));
       + IF srEng==2 AND ta<0.005 THEN
       + crL:={crH,
       + "",
       + "q crit = "+sr_FmtF(qc,4),
       + "PPL fallback at α < 0.005:",
       + "q crit may be off by a few %",
       + "Reject H0 for a pair if q ≥ q crit",
       + "(matches Harter 1959 tables)",
       + "Stored in EP_QCRIT"};
       + ELSE
       + crL:={crH,
       + "Stored in EP_QCRIT"};
       + END;
       + sr_ShowRes("Critical value q(α)",crL,"any key or tap = menu",1);
       + LOCAL qc,hsd,xb,hk,hdH,hdL;
       + hdH:="α = "+sr_FmtF(ta,4)+"  k = "+STRING(tk,2,0)+"  m = "+STRING(tm,2,0)+"  ν = "+STRING(vv,2,0);
       + IF srEng==2 AND ta<0.005 THEN
       + hdL:={hdH,
       + "MSE = "+sr_FmtF(tmse,4),
       + "q crit = "+sr_FmtF(qc,4),
       + "HSD = q·√(MSE/m) = "+sr_FmtF(hsd,4),
       + "PPL fallback at α < 0.005:",
       + "q crit, HSD may be off a few %",
       + "|"+xb+"i - "+xb+"j| > HSD → significant",
       + "Stored in EP_QCRIT, EP_HSD"};
       + ELSE
       + hdL:={hdH,
       + "Stored in EP_QCRIT, EP_HSD"};
       + END;
       + hk:=sr_ShowRes("Tukey HSD",hdL,"Ent or tap = yardstick, Esc = menu",1);
       + "After Dunlap et al. 1977, BRMI 9(4)",
       + "Teaching aid; not for legal use."},
  ok   10e every removed code line is one of the sites listed (the X counters, the two pages' heads and tails, the HSD line, the About page's last line): [] 
  ok   10e the added lines include every fix (counters, notes, S3 line, disclaimer) 
   10f. names and static rules
  ok   10f the new names bzI, cbI, crH, crL, hdH, hdL are not built-ins (case ignored) 
  ok   10f no LOCAL line declares more than 6 names: [] 
   10g. the review's own check_StudRange.py, on beta 15c and on beta 15d
     beta 15c: 0 checks failed []; ['   k 20, df 3, alpha 1E-4 on the fallback: q crit 94.658 (SciPy 92.944) -- the qhi < 60 cap in sr_SRCrit']
     beta 15d: 0 checks failed []; ['   k 20, df 3, alpha 1E-4 on the fallback: q crit 94.658 (SciPy 92.944) -- the qhi < 60 cap in sr_SRCrit']
  ok   10g the review's check_StudRange.py passes on beta 15c and on beta 15d (0 failures each) 
     harness runs of beta 15d: 71; scientific STRING results: none
  ok   8c no STRING(x, 2, d) of beta 15d printed in scientific form in any of the 71 runs (9 and 10 included) []
     (beta 15a in this script: [(-0.0, 1, '-2.8ᴇ-17'), (1e-10, 4, '1.0000ᴇ-10'), (2.757e-07, 5, '2.75700ᴇ-7'), (1.3651558e-05, 4, '1.3652ᴇ-5'), (0.0004, 3, '4.000ᴇ-4')])

CHECKS: 160
FAILURES: 0 []
