`affordabilityGrid` validated `baselineRate` and nothing else, so a
`baselineTrials` of 0 reached `twoSamplePriors` as 0 successes in 0
trials. Its null prior is then Beta(1, 1), and the grid priced the whole
suite against "every case passes half the time", which is the
invented-baseline failure `src/baseline.ts` exists to refuse.
It did not fail loudly. At 20 cases and a 15-point MDE it answered a
complete cell, 504 runs and $1.06 a pull request, `affordable: true`, on
a cheaper typical bill than the same call at a real 60-run baseline (196
runs against 288): the cheapest row on the page was the one with no
baseline behind it at all. A fractional or negative count did reach a
refusal, but from `requireCounts` three frames down, naming a success
count the caller never passed.
`planCase` returns IMPOSSIBLE for a case with no baseline,
`evaluatePoint` throws on `baselineRuns < 1` and `gate` throws
PEEKSAFE_E_BASELINE_MISSING. This was the one planner entry point that
checked nothing, and it now refuses a `baselineTrials` that is not a
positive integer.
`test/budget.test.ts` pins the three refusals and that a recorded
baseline still prices, so the guard cannot be widened back into silence.
What broke
affordabilityGridis the one planner entry point that never checked its baseline argument. It validatesbaselineRateand stops there, sobaselineTrials: 0flowed throughsamplesForEvidenceintotwoSamplePriorsas 0 successes in 0 trials. The null prior there is Beta(1, 1), so the grid priced the whole suite against "every case passes half the time" and reported a budget for it.It did not fail loudly, which is the part that matters. Measured before the fix with
affordabilityGrid(0.85, 0, DEFAULT_PLAN, 5, [20], [0.15]):A complete, affordable-looking cell, and a cheaper typical bill than the same call at a real 60-run baseline: 196 runs against 288. A Beta(1, 1) null puts
p0at 0.5, so the SPRT decides faster and the grid quotes less. The cheapest row on the page was the one with no baseline behind it at all.A fractional or negative count did reach a refusal, but three frames down in
requireCounts, reporting a success count the caller never passed:affordabilityGrid(0.85, -10, ...)threwtwoSamplePriors: need 0 ≤ successes ≤ trials, got -8/-10.Why it is a defect and not a judgement call
This is the library's central invariant applied to an entry point that missed it, which
brain/architecture/overview.mdsays most merged fixes have been. The other three entry points for the same question all refuse:planCasereturnsIMPOSSIBLEwithreason: 'no baseline: nothing to compare against, record one first'(src/plan.ts).evaluatePointthrowsPEEKSAFE_E_CONFIGonbaselineRuns < 1(src/frontier.ts).gatethrowsPEEKSAFE_E_BASELINE_MISSING(src/gate.ts), andsrc/baseline.ts's header documents the prototype bug this exists to prevent: a missing baseline read as a Beta(1, 1) posterior median of 0.5.affordabilityGridis public, exported fromsrc/index.tsand named in the README's API section.The fix
One guard in
affordabilityGrid, refusing abaselineTrialsthat is not a positive integer, with the same error code and message shapeevaluatePointalready uses for the same argument. Nothing else changed: no statistics moved, so nopaper.test.tsnumber and no README figure moved.What proves it cannot break the same way
A new case in
test/budget.test.ts, which fails onmain(expected [Function] to throw an error) and passes here. It pins all three refusals (0, negative, fractional) and asserts that a recorded baseline still prices, so the guard cannot be widened back into silence by deleting an assertion.brain/architecture/planner.mdgains a line under "Things that have been wrong before".Checks
Run on this runner, all green:
npm run typechecknpm test(12 files, 237 tests)npm run buildNot run here, and left for CI: Node 22 (this runner is Node 24.21.0), and the packed-tarball dependency check in
.github/workflows/ci.yml. There is no Docker daemon on this runner, though nothing in this repository's checks needs one.Not in flight
I read the whole open pull request queue with
gh api repos/sferarc/peeksafe/pulls --paginateand no row limit: 3 rows, #37release/0.1.0, #39chore/pnpm-node26, #40chore/biome-lefthook. None touchessrc/plan.tsand none references an issue for this. There are no openhq-queueissues and nothing carrieshq-changes-requested. No issue describes this defect, so there is none to reference; the files changed are named above for the next shift's search.Follow-up I found and did not take
evaluatePointinsrc/frontier.tsguards itsbasislookup withbasisCost === undefined, while its siblingcostSharesin the same file usesObject.hasOwnand says in a comment exactly why:BILLS['toString']is inherited fromObject.prototype, so it is notundefinedand slips through anundefinedcheck.evaluatePointstill has the weaker guard. Measured on this branch withbasis: 'constructor'at 20 cases, a 25-point MDE and a 480-run baseline:costSharesrefuses the identical input. It is a narrow path, a plain typo gives a key that isundefinedand does refuse correctly, so it is hardening rather than a reachable budget error, which is why I left it out rather than widening this diff. A one-line change from=== undefinedtoObject.hasOwn, plus a test, would close it.