Occams · reviews

occams-test-lab — FIXES-AND-TESTS

Published copy, 2026-10-03. An external review of this repository at 2e86d58, written as the defect catalogue of the successor lab and committed here on the author's instruction (TASKS-v4.md M16.2). Two sentences are reworded where this repository's publication gate requires, each marked in place; nothing else is changed. What was checked and what reproduced is in 2026-10-03-inference-review-verification.md. What this lab does about each fix — built here in three releases, or left to the successor with the reason — is M16 in TASKS-v4.md. Where this text and M16 differ, M16 is what was done.

RepositoryMaverickHQ/occams-test-lab, package occams v1.0.0
Commit reviewed2e86d58 (HEAD, 2026-10-03 16:09 +0100); donor prop-challenge-lab@cfc5af8
Date2026-10-03
Scopeoccams/ (15.8k LOC), tests/ (7.9k), tools/, scripts/, register/*.jsonl, docs/, CI. Read-only review. The verdicts were not re-run (the licensed archive and occams.toml/configs/ are not in the public tree).
Baseline on this commitpytest: about 652 tests pass (about 5.5 min full; about 75 s with -m "not slow"). ruff, credscan, prepublish and vendor_core --verify/--verify-donor are clean. All 6 Registers verify, and their heads match docs/LAB-CONCLUSION.md. make null and make signal behave as documented on both engines.

Evidence legend.

Severity legend.

SeverityMeaning
P0Correctness of verdicts. The test a guard performs is not the test it claims, or its error rate is not the declared alpha.
P1Integrity and governance: the Register, the alpha ledger, identity of the code that measured, and the apparatus's own calibration.
P2Realism: what the simulators and data assume about the market.
P3Hygiene and docs.

Idiom. Every fix is OTL-Fnn. Every task is OTL-Tnn. Every test is written red first: the doc names at least one test per fix that FAILS on 2e86d58 and passes after the fix. A test marked @slow runs in make check and in CI, never in make test-fast. Refusals are loud and named. No fix introduces a default for an author's number (R9). Where a fix needs a new number (a confidence level, a ceiling), it is a required config key with no default, and python -m occams --schema prints the key and never a value.


1. Governance — how fixes reach a lab whose verdicts are sealed

Six rules, to be adopted as ADR-0047 before any code in §3 lands.

G1 — Fixes bind forward, by ADR. Each P0, P1 and P2 fix is adopted by an ADR and binds from the next registration onward, as ADR-0043 and ADR-0045 did. Reserve the range ADR-0047 to ADR-0060:

ADRTitle (proposed)Fixes
0047Fixes bind forward; re-scores are diagnostics, never verdictsgovernance (this section)
0048The null is drawn at the winner's N, by calendar date, with p counted plus oneF01, F02, F03
0049The fifth check is side-matched and dependence-aware; execution is paired, selection is randomisedF04
0050The floor is cleared by a lower confidence bound; power is planned against a declared alternativeF05, F06
0051A plateau is judged in standard errors, without the winner in its own medianF07
0052A survey ranks under a max-statistic null; screen, validate, then measureF08, F18
0053Overlap is structural, and the lab has one alpha ceilingF09, F10
0054The Register's heads are pinned, signed and anchored outside the repositoryF11, F12
0055The engine is identified by content; a dirty tree cannot measureF13, F14
0056A universe is point-in-time, or it carries a measured survivorship diagnosticF15 (amends ADR-0025)
0057Costs are measured or swept; stops pay slippageF16 (amends D23 / M5.0's closure)
0058The calibration gate runs on every engine before it measuresF17, F23
0059Entry kinds are modules in a closed registryF21 (amends ADR-0037)
0060reserved—

G2 — Sealed verdicts are never edited.

G3 — A re-score is a diagnostic, in its own store.

G4 — Re-score at a clean commit. The stamped verdict commits are not in the public repository [V]: git cat-file -e fails for e2cdf14 (Q-003), b145d49 (Q-004), e05a659 (Q-005), 1a243e5 (Q2-001) and 8f81ff3 (Q2-002). reproduce private --at-commit (occams/reproduce.py:175-188) therefore fails "cannot check out" on the public tree. The procedure runs in the author's private history repository, beside the licensed archive:

  1. python -m occams reproduce private --question Q2-001 --register register/programme-2.jsonl --queue register/programme-2-queue.jsonl --archive archive --config configs/programme-2.toml --at-commit - It must print an exact reproduction: N, winner cell, EV to 1e-12, refusals. - If it cannot, the re-score stops and records reproduced=False with the reason. Never re-score what you cannot first reproduce. - Both P2 verdicts carry -dirty stamps (programme-2.jsonl #15, #19, #28, #33). compile.py:136-142 attributes this to the loop's own Register writes. Reproduction at the bare commit is then the test of that claim.
  2. Check out the clean commit that implements the ADRs being applied. git status --porcelain -- occams tools pyproject.toml must be empty (F13).
  3. python -m occams reproduce private --question Q2-001 … --rules 0048,0049,0050 --diagnostic register/diagnostics.jsonl. - This is a new flag. It re-measures with the same seed, same partition and same sweep, evaluates the guards under the named ADRs, and appends one Rescored. - It refuses if the diagnostics store's head is not the pinned head (F11).
  4. Commit register/diagnostics.jsonl and the updated register/HEADS.toml, then sign the heads (F11).

G5 — Conservative fixes before non-conservative ones.

G6 — The apparatus proves itself before and after every fix.


2. Summary of fixes

Column 6 asks: does this change recorded numbers? No means displays, records or refusals only. Numbers move means p-values, SEs or EVs change but no outcome is expected to flip. Verdict at risk means a recorded outcome or readiness table could change under a G3 re-score.

IDTitleSevFilesEffortChanges recorded numbers?
OTL-F01position_boxed null and always-long baseline resampled at the pool's N, not the winner'sP0engine/position_boxed.py:163-219,253-255; survey/candidates.py:403-405SNumbers move (Q-005, Q2-001, Q2-002 p-values; readiness)
OTL-F02Nulls ignore same-date dependence; position_boxed blocks on per-name bar indexP0engine/day_boxed.py:331-338,361-369; engine/position_boxed.py:198,217MNumbers move
OTL-F03Monte Carlo p without +1; passing evidence never recordedP0guards/beats_null.py:26-27; guards/beats_always_long.py:30-31; survey/candidates.py:412; guards/forward.pySNumbers move (tiny)
OTL-F04Fifth check is side-blind and dependence-blind (a coin flip passes it)P0guards/beats_always_long.py; engines' baseline functionsMVerdict at risk (Q2-001, diagnostic only)
OTL-F05Floor judged on the winner's point estimateP0guards/clears_floor.py:14; hypothesis.py:70-75; core/power.py (call sites only)MVerdict at risk (Q2-001)
OTL-F06Power promised at the template's σ, not the winning cell'sP0guards/measure.py:16-27; survey/candidates.py:318SVerdict at risk (Q2-001)
OTL-F07Plateau slack is absolute and the winner sits in its own medianP0guards/plateau.py:30-32SNumbers move
OTL-F08Survey's fifth check is selection-blind over 38,976 cells; no validate step; "halves" wordingP0survey/candidates.py:349-419; survey/run.py; README.md:128,134,178,189LVerdict at risk (grid-001/002 readiness tables)
OTL-F09Overlap gate bypassed by any free-text distinction and by a change of axisP1ledger/alpha_budget.py:223-259; survey/candidates.py:343,710MNo
OTL-F10Accrual outside the declared total; no lab-wide alpha ceilingP1ledger/alpha_budget.py:134-140,195-205; config.pyMNo
OTL-F11Register is a keyless chain: forgery and truncation verify; heads pinned nowhereP1register/store.py:103-130; new register/HEADS.toml; tools/prepublish.pyMNo
OTL-F12Append is unlocked and quadraticP1register/store.py:97-136SNo
OTL-F13Engine identity: -dirty/unknown accepted; content hash covers a hand list missing fills, sizing, guardsP1core/archive.py:97-106 (vendored, wrap only); spec/compile.py:136-164; survey/run.py:47-59SNo
OTL-F14Stamped commits absent from the public history; at-commit reproduction impossibleP1reproduce.py:175-200; new SOURCES.tomlMNo
OTL-F15Survivorship: current-member universes measured on 1993–2019P2data/universe.py, data/partitions.py, UniverseDeclaredLVerdict at risk (Q2-001, either direction)
OTL-F16Costs are a declared bound never measured; stops fill at the levelP2costs/equity.py:53-58; engine/day_boxed.py:250; engine/position_boxed.py:107MNumbers move (Q2-002 knife-edge on a point floor)
OTL-F17Vendored calibration gate unusedP2core/calibration.py (vendored, unused); new occams/calibrate.pyMNo
OTL-F18Baseline non-stationarity: definition is the oldest 30 %P2survey/candidates.py readiness; survey/run.py:220-225 (_eras)SVerdict at risk (readiness)
OTL-F23Controls show "refused once", not the guards' false-positive rateP1tests/test_controls.py:45-54; controls.py; controls.toml; CIMNo
OTL-F19Dead vendored modules; boto3 path reachableP3occams/core/*, PROVENANCE.json, tests/test_provenance.pySNo
OTL-F20Probes and the fifth check duplicated across engines and surveyP3engine/day_boxed.py:306-369; engine/position_boxed.py:188-225; survey/candidates.py:349-419MNo
OTL-F21Entry kinds are an if/elif chain across enginesP3engine/day_boxed.py:123-170; engine/position_boxed.py:27-28; spec/spec.pyMNo
OTL-F22Stale stringsP3Makefile:48; controls.toml:18; README.md:128,134,178,189SNo
OTL-F24Lint too narrow; small smellsP3pyproject.toml; ledger/alpha_budget.py:25,32; guards/leave_one_out.py:23-26SNo
OTL-F25TASKS diary and CLAUDE.md sizeP3TASKS-v4.md, CLAUDE.mdMNo

F23 sits with the P1s because it is the apparatus's own calibration, and it would have caught F01, F02 and F04.


3. The fixes

OTL-F01 — the position_boxed nulls are resampled at the pool's N, not the winner's (P0)

Problem [V]. day_boxed resamples its null and baseline with n_trades=max(winner.n, 1) (engine/day_boxed.py:401-402). position_boxed does not.

# engine/position_boxed.py:171-176  block_bootstrap_means(x, ...)
n = x.size
...
idx = (starts[:, :, None] + np.arange(block)[None, None, :]).reshape(draws, -1)[:, :n]
return x[idx].mean(axis=1)

Why it matters [V-probe]. Probe A1 (Appendix A) uses a pure martingale world: 8 names, 1,260 days, σ 2 %/day, zero costs, a single cell (k = 1), down_run(3), hold 5, 200 seeds. Winner N is about 645 against a random-entry pool of about 2,035.

α = 0.01α = 0.05
beats-null false-positive rate, as built0.1400.225
beats-null, resampled at the winner's N (F01 only)0.0150.100
beats-always-long false-positive rate, as built0.0650.135

Fix.

def block_bootstrap_means(x: np.ndarray, *, block: int, draws: int, seed: int, n: int | None = None) -> np.ndarray:
    """... each draw concatenates blocks to length n (default: len(x)) and takes its mean."""

Tests — tests/test_null_sizing.py (new):

Impact on recorded results. The p-values of every position_boxed check widen; none was recorded when passing (F03).

Migration. ADR-0048. Measurement gains two fields with defaults, and the Register format is unchanged. The survey index gains null_n per cell. Old survey results stay valid as records but are re-labelled "pre-ADR-0048" on the readiness page.


OTL-F02 — the nulls ignore same-date dependence (P0)

Problem [V].

Why it matters [V-probe]. Probe A2 uses the day_boxed engine: 10 names, 1,260 days, σ 2 %, martingale, down_run(2) intraday, single cell, zero cost, 200 seeds. With ρ = 0 the null is exact; with a common factor ρ = 0.5 it is badly oversized.

common-factor ρfalse positives at α = 0.01at α = 0.05
0.00.0100.050
0.50.0900.200

ρ = 0.5 is typical of large-cap daily returns. The registered ρ values come from the signals' same-day net R and are lower, but the deff is 1 + (m−1)ρ with m names firing per date. For a 98-name universe after a sell-off, m is large.

Fix. One function in occams/inference.py (new; see F20):

def dependent_null(pool: Sequence[Box], *, winner_dates: Sequence[int], n: int, draws: int, seed: int,
                   side_rule: Literal["coin", "matched", "long"], block: int | None = None) -> np.ndarray:
    """Date-cluster moving-block bootstrap. Group the pool's boxes by calendar date. Resample
    blocks of consecutive dates (block length: core.stats.optimal_block_length on the pool's
    per-date mean series, at least the hold length). On each sampled date draw k boxes, k taken
    from the empirical distribution of the winner's per-date counts, so a draw clusters as the
    winner clusters. Concatenate to n; return per-draw means. Side per side_rule."""

Tests — tests/test_dependent_null.py:

Impact on recorded results. Conservative: the null widens. Q-003 recorded p = 0.983-class failures (Q-004 #15: p_null 0.983, null mean −0.034) and stays refused. For Q2-001, the z of 3.8–9.9 computed under F01 already assumed deff ≤ 2, so it still passes. Survey readiness: see F08.

Migration. ADR-0048. The survey index gains null_method: "date_cluster_block".


OTL-F03 — p without +1, and no evidence recorded when a check passes (P0)

Problem [V].

Fix.

Tests:

Impact. Readiness pages print ≥ 0.00025 (1/4001) instead of 0.0000. No outcome changes: with B ≥ 2,000 the floor of p is ≤ 0.0005 < 0.01.

Migration. ADR-0048. GuardEvidence is a new @register_record. Its field names pass the S7 money check (register/store.py:28-51).


OTL-F04 — the fifth check is side-blind and dependence-blind (P0)

Problem [V].

make null --engine=day_boxed → checks that passed: ['plateau', 'leave_one_out', 'beats_always_long']
winner stop 4 target 3: n 6295, EV −0.0797; always-long at this geometry −0.1033; margin +0.0236
baseline draws: mean −0.1031, sd 0.0090  →  z ≈ 2.6 → p ≈ 0.005 < α/9 = 0.0056

Design note. The coordinator's phrase "paired on shared boxes" is right for one half and wrong for the other.

Fix.

Tests — tests/test_beats_always_long.py:

Impact.

Migration. ADR-0049 amends ADR-0043 (the baseline is side-matched) and ADR-0045 (the margin is side-matched). The Cell.baseline_ev semantics change, so add Cell.baseline_rule: str = "always_long" so that old measurements read correctly.


OTL-F05 — the floor is judged on the winner's point estimate (P0)

Problem [V].

# guards/clears_floor.py:14
if w.ev < floor.ev_net_r:

Why it matters. Bonferroni-simultaneous lower confidence bounds at α/k cover all k cells' means jointly with probability ≥ 1−α. Requiring LCB_{α/k}(winner) ≥ floor is therefore valid after selection, and it removes the winner's curse without a separate correction. Here k = search_space_size, matching plan.alpha/k in the other guards.

Fix.

def check(m, floor, plan, search_space_size) -> Refusal | None:
    w = m.winner
    a_c = plan.alpha / search_space_size
    se = se_cluster(w.trades, by="day", hac_lag=hold_of(w))          # occams/inference.py
    lcb = w.ev - z(1 - a_c) * se                                     # one-sided
    if lcb < floor.ev_net_r: refuse("floor: the lower confidence bound on EV is below the declared floor (ADR-0050)", ...)
    ...frequency half unchanged...
σ (R)alternative − floor = 0.05alternative − floor = 0.10
1.20 (programme 1)5,7811,446
1.43 (Q3-001)8,2542,064
2.22 (Q2-001 plan)19,8014,951
4.09 (hold-20 sibling)67,30816,827

This is the honest cost of "clears the floor". Expect far fewer questions to be affordable. The readiness tables' "lowest affordable floor" column must be recomputed under the new formula.

Tests:

Impact [V numbers, I σ]. Using the recorded EV and N and the plan σ:

questionEVNσ usedLCB at 0.99, iidat deff 1.5at deff 2floor 0.15
Q2-001+0.21346,9462.22 (plan)+0.151+0.138+0.126knife-edge → refuse
Q2-001+0.21346,9464.09 (hold-20 sibling)+0.099+0.073+0.052refuse
Q2-002+0.10912,7322.2–4.09≤ +0.07——refuse (already)
Q-005+0.0518931.2−0.042——refuse (already)

Migration. ADR-0050. HypothesisRegistered.alternative_ev_net_r is a new field with a default. whatif and power call sites change, while the vendored core/power.py stays byte-identical: the one-sided formula lives in occams/inference.py or hypothesis.py. The quickstart's step 4 ("714") stays as the M0.15 known answer, labelled pre-ADR-0050.


OTL-F06 — power is promised at the template's σ, not the winning cell's (P0)

Problem [V].

Fix.

Tests:

Impact. Q2-001: medium–high probability of a REGISTERED→MEASURED refusal on re-score [I]. This holds if its hold-20 gated σ is above 3.66 R; the ungated sibling's 4.09 R suggests so. The private PathsArchived sha 2a87f507… (P2 #18) holds the trades and settles it in one line. Q3-001 (already refused for N) stays refused. Q2-002 is unaffected (null on the floor).

Migration. ADR-0050. No Register format change beyond F03's evidence.


OTL-F07 — plateau slack is absolute, and the winner sits in its own median (P0)

Problem [V].

# guards/plateau.py:30-32
score = m.score(w)
med = median(m.score(c) for c in neigh if c.trades)      # neigh includes w (line 21)
if score - med > gates.plateau_slack:                     # 0.10 for every question (queue gates)

Fix.

Tests:

Impact. Numbers move. The cell EVs of the P2 sweeps are in the private PathsArchived files, not in the Register. Probability of flipping Q2-001's plateau: low–medium [I]. Q-004's plateau was refused for neighbourhood size and stays refused.

Migration. ADR-0051. Gates.plateau_slack_se is required for new registrations; it is absent on old ones and read as "pre-ADR-0051".


OTL-F08 — the survey's fifth check is selection-blind; no validate step; the wording says "halves" (P0)

Problem [V].

What the records say [V]. The two measured shrinkages fail in different ways. Selection-aware inference fixes only one of them.

definition EV → measured EValways-long, definition → measuredmargin, definition → measured
Q2-001 (#24)0.256 → 0.213 (−0.042)0.017 → 0.214 (+0.197)0.239 → −0.001
Q2-002 (#35)0.333 → 0.109 (−0.224)0.132 → 0.034 (−0.098)0.201 → 0.075

Fix.

  1. Max-statistic inference over the screen. Use a Romano–Wolf step-down on studentised margins (preferred: it controls FWER and is less conservative than Bonferroni), or Hansen's SPA when only "is the best real?" is wanted. - Build the cell × date matrix of per-date margin contributions on the screening set. - Bootstrap dates in moving blocks (F02), with one shared index draw for all cells, so the cross-cell correlation is preserved. - Each cell's adjusted p replaces p_baseline in readiness. The threshold [survey].fwer_alpha is required and has no default. - Memory: about 2,500 definition dates × 38,976 cells ≈ 780 MB float64. Do it per family on a laptop, or whole on the burst instance (tools/aws/burst.sh).
  2. Screen / validate / measure. Split the definition partition in time order: screen on eras 1–2 and validate on era 3. - survey/run.py:220-225 _eras already cuts three eras. - A cell is "ready" only if its validate-era margin, tested once per cell at the family-wise level of the top-K carried forward, clears its threshold. - The measurement partition is untouched (D13), and the frozen calendars (ADR-0038) do not move.
  3. Shrinkage in the table. Print an empirical-Bayes shrunk margin per cell: normal–normal, with the prior fitted on the screen's cell distribution. Print the shrinkage the recorded Q2 pairs imply (Δ −0.240, −0.127) as the realised prior.
  4. Wording. - README: "on the definition partition — the oldest 30 % of the frozen calendar". - README: "on the measurement partition — the next 50 %, read once". - docs/EXTENDING.md and docs/SETUP.md: same.

Tests — tests/test_survey_inference.py (new):

Impact. Readiness tables only; no verdict.

Migration. ADR-0052 amends M12.3/M12.4. SurveyRecorded gains inference: "romano_wolf" and fwer_alpha. Old readiness pages stay as committed and are labelled "pre-ADR-0052".


OTL-F09 — the overlap gate is bypassed by any free-text distinction, and by a change of axis (P1)

Problem [V].

# ledger/alpha_budget.py:225  overlap(): different axis → 0.0
if new.axis is not old.axis or new.partition != old.partition:
    return 0.0
# :253  gate(): any non-empty text passes
if supersedes == old.hypothesis_id or (distinction or "").strip():
    continue

Fix.

Tests — tests/test_ledger.py:

Impact. None on recorded verdicts (prospective). Recorded diagnostic: Q2-002 would have needed supersedes: Q2-001 and been charged against the sp_100 measurement-partition budget.

Migration. ADR-0053. HypothesisRegistered.distinction becomes a typed dict. Old records hold a string, which readers treat as kind="legacy_text".


OTL-F10 — accrual outside the declared total; no lab-wide alpha ceiling (P1)

Problem [V].

Fix.

Tests — tests/test_ledger.py:

Impact. No verdict changes. The console shows lab-level spent/ceiling.

Migration. ADR-0053. Add LabCeilingDeclared(ceiling, registers, decided_by) as the first record of a new register/lab.jsonl, or of diagnostics.jsonl if you prefer a single side store.


OTL-F11 — the Register is keyless: forgery and truncation verify; heads pinned nowhere (P1)

Problem [V, V-probe]. The digest is sha256(prev|seq|canonical(payload)) with no key (register/store.py:103-104). Probe A4:

The heads (c2706a09716b, 23cf22aa6586, e92fda14a7f1) appear only in prose (docs/LAB-CONCLUSION.md:6-8, the HTML pages, and TASKS). No test reads them. The public history is a single snapshot (17 commits, all 2026-10-03), so git cannot show registration before measurement. The at timestamps are self-reported.

Fix.

  1. Pinned heads. register/HEADS.toml, one table per store: path, count, head_sha, pinned_at. - tools/prepublish.py and a new test fail on any mismatch. - Appending updates HEADS through occams register pin. The loop does it after its last append.
  2. Signed heads, third-party verifiable, no new Python dependency. - ssh-keygen -Y sign -f <the author's key file> -n occams-register register/HEADS.toml (reworded for publication: the original names a path under the home directory) produces HEADS.toml.sig. - Commit register/allowed_signers. - Verify with ssh-keygen -Y verify -f register/allowed_signers -I author -n occams-register -s register/HEADS.toml.sig < register/HEADS.toml. GitHub runners have OpenSSH. - Also push a signed tag register/<store>/<count> per pin.
  3. External anchor. ots stamp register/HEADS.toml (OpenTimestamps, optional tool). Commit the .ots proof, then ots upgrade later. - This dates the heads independently of GitHub and of the author's clock. - When the tool is absent, make anchor refuses loudly; it never skips silently.
  4. Keyed MAC (optional, private). mac = HMAC-SHA256(OCCAMS_REGISTER_KEY, sha) beside sha on each line. It proves integrity to the key holder only, so it ranks below 2 and 3.
  5. Backfill. Pin today's six heads as they verify now. The anchor dates the pin, not the past. Say so in ADR-0054: the pre-pin history is attested by the private repository and the dated pages, not by this mechanism.

Tests — tests/test_register_anchor.py:

Impact. None on numbers. It closes the gap between "append-only" (true in code) and "cannot be rewritten undetectably" (false today).

Migration. ADR-0054. The line format is unchanged. HEADS, the signature, allowed_signers and the .ots proof are side files. CI adds a "verify signed heads" step.


OTL-F12 — append is unlocked and quadratic (P1)

Problem [V].

Fix.

Tests:

Impact. None. Migration. None (behavioural).


OTL-F13 — engine identity: -dirty and unknown accepted; the content hash covers a hand list (P1)

Problem [V].

Fix.

Tests:

Impact. None on numbers. Future records become reproducible by content.

Migration. ADR-0055. The vendored core/archive.py stays byte-identical and the lab stops calling its engine_sha directly. test_provenance is untouched.


OTL-F14 — the stamped commits are missing from the public history (P1)

Problem [V]. See G4. The five verdict commits are not reachable from the public repository (git cat-file -e fails). reproduce private --at-commit (reproduce.py:175-188) cannot run for anyone but the author, in a different repository. docs/PUBLICATION.md's amendment of 2026-10-03 records the decision that the public repository carries no history.

Fix. Keep the decision, and make the sources addressable without the history:

Tests:

Impact. None. Migration. ADR-0055.


OTL-F15 — survivorship: current members measured on 1993–2019 (P2)

Problem [V].

Fix.

Tests:

Impact (non-conservative).

Migration. ADR-0056 amends ADR-0025. New record kinds UniverseMembership and SurvivorshipDiagnostic.


OTL-F16 — costs are a declared bound never measured; stops fill at the level (P2)

Problem [V].

Fix.

Tests:

Impact (non-conservative, [V] numbers).

Migration. ADR-0057 amends D23 and reopens M5.0 as "measured, or swept with the curve recorded".


OTL-F17 — wire the vendored calibration gate (P2)

Problem [V]. occams/core/calibration.py (308 LOC) is imported by nothing in the lab (grep occams.core outside core/). Its contract, an estimator returns about 0 on a world with no effect and recovers a planted one, is exactly what F01, F02 and F04 violated without any test noticing.

Fix.

Tests:

Impact. None. Migration. ADR-0058. New record ApparatusCalibrated in register/diagnostics.jsonl, or in the programme Register for new programmes.


OTL-F18 — baseline non-stationarity: the definition partition is the oldest 30 % (P2)

Problem [V]. The always-long return at a geometry is a property of the era (PROGRAMME-2-CONCLUSION.md:93). At hold 20 in the up regime it was +0.016 on 1993–2003 and +0.214 on 2003–2019. The readiness tables show "Always-long, survey → now" only on definition.

Fix.

Tests:

Impact. Readiness only. Q2-001's survey cell had era margins +0.186 / +0.331 / +0.169 (readiness row 1). Its baseline instability inside definition is unknown from the committed page [I]. The 0.016 → 0.214 jump happened across the partition boundary, so this fix can only flag a trend that is already visible inside definition.

Migration. ADR-0052.


OTL-F23 — controls show "refused once", not the guards' false-positive rate (P1)

Problem [V].

Fix. Add a size and power table (occams/calibrate.py, make calibrate, nightly CI schedule: job running -m slow). Each guard is run in isolation on:

worldpurpose
martingale, zero cost, ρ ∈ {0, 0.5}size of beats-null
drift so always-long > 0, long-only entry with no timing skillsize of the fifth check
true EV = floorcoverage of the LCB floor
planted at the alternativepower

Assert, for S seeds, size ≤ α + 2.33·sqrt(α(1−α)/S) and power ≥ planned − 2.33·sqrt(p(1−p)/S). S ≥ 200 for size, S ≥ 100 for power. Probes A1 to A3 are the starting point.

Tests. The @slow size and power tests listed under F01, F02, F04, F05 and F08 live in tests/test_apparatus_size.py. Also:

Impact. None on records. Makes S3 and S10 meaningful.

Migration. ADR-0058. CI: the nightly job, and a weekly re-run of make calibrate on main.


OTL-F19 — dead vendored modules; boto3 path reachable (P3)

Problem [V].

Fix. Keep the vendored core byte-identical: its provenance is a feature.

Tests. Both above; test_core_usage is RED until written, then GREEN.

Impact. None. Migration. None. test_provenance (12 modules) is unchanged.


OTL-F20 — probes and the fifth check are duplicated (P3, but do it first in phase 2)

Problem [V]. The always-long probe, the random-entry probe, null building and the fifth check exist in three places:

Every P0 fix would otherwise be made three times.

Fix.

Tests:

Impact. None. Migration. None.


OTL-F21 — entry kinds are an if/elif chain (P3)

Problem [V].

Fix.

Tests:

Impact. None. Migration. ADR-0059 amends ADR-0037. The kinds stay closed by decision.


OTL-F22 — stale strings (P3)

Problem [V].

Fix. Edit the strings.

Tests (tests/test_docs.py):

Impact / Migration. None.


OTL-F24 — lint too narrow; small smells (P3)

Problem [V].

Fix.

Tests:

Impact / Migration. None.


OTL-F25 — TASKS diary and CLAUDE.md size (P3)

Problem [V].

Fix.

Tests:

Impact / Migration. None. TASKS history stays in git and docs/log/.


4. Will these fixes change the lab's results?

Principle. The fixes in §3 split into two kinds:

Therefore every null verdict survives every conservative fix by construction. What is at risk is the one positive verdict (Q2-001) and the readiness tables. Non-conservative fixes are applied after the conservative bundle (G5), so a null cannot be revived by a cheaper cost assumption on a guard set already known to be anti-conservative.

Caveat. None of this was re-run. The licensed archive, occams.toml/configs/ and the PathsArchived trade files are private. Every number below is either read from the Register [V] or derived from it with the stated assumption [I]. The G4 procedure settles each one.

RecordAs reachedFixes that touch itExpected directionP(outcome flips under a G3 re-score)Reasoning
Q-003 (P1 #9)null: beats-null, floor, leave-one-out (2 groups)F01–F05, F16stays null; more refusals≈ 0EV −0.047 on 1,391. LCB99 ≈ −0.12. Measured ETF spreads add at most +0.033 R at stop 3 %. Leave-one-out with 2 groups is structural.
Q-004 (P1 #21)null: all fourF02, F03, F05, F07, F16stays null≈ 0EV −0.045 on 2,088. Recorded p_null 0.983 (#15), null mean −0.034. Widening the null cannot help a winner below the null mean.
Q-005 (P1 #32)null: floor, leave-one-out (IWM; 0.014 without)F01, F02 (position_boxed), F05, F16stays null; beats-null may also refuse≈ 0EV +0.051 on 893 with σ 1.2: LCB99 −0.04. Costs add at most +0.033 R, so EV 0.084 < 0.15. Leave-one-out unchanged.
LabClosed (P1 #33)falsifier fired on 3 nullsnonestands0It rests on three nulls that cannot flip.
Q2-001 (P2 #19)supported (four checks); margin −0.001 recordedF04 (certain), F05, F06, F01/F02 (pass), F07, F15, F16toward refusalHigh (> 80 %) under the conservative bundleSee below.
Q2-002 (P2 #33)null on the floor (0.109 < 0.15)F01, F02, F04, F05, F15, F16stays nullVery low with F05 in force; low withoutIt needs +0.041 R. Measured costs supply it only if the round trip is ≤ 1.8 bp. Under F05 its LCB is ≤ about +0.07. Point-in-time membership likely lowers the EV.
Q3-001 (P3 #12)refused REGISTERED→MEASURED: 863 < 1,068F05 (alternative-based power), F06stays refused, by more≈ 0required_n rises with any alternative: σ 1.434 and alternative − floor 0.10 give 2,064. ETFs, so no point-in-time effect.
grid-001 readiness (2026-09-18)20 of 20 passF01 (multi-day rows), F02, F03, F08, F18toward fewer passesMedium–high that the table changes; low that it emptiesLarge-N sp_100 and dow_30 cells have z ≈ 5–6.5 iid against the Bonferroni bar of 4.70, so some survive Romano–Wolf if dependence is mild. ETF cells and date-clustered single-name cells (deff ≈ 2.5) mostly fall.
grid-002 readiness (2026-09-20)20 of 20 passsamesameMedium–highSame structure. Row 1 (dow_30, margin +0.371) is the likeliest survivor [I].
ProgrammeStopped P2 #36, P3 #13author's stopsF10 (display only)stand0The stops are author's acts. Balances are shown lab-wide only.

Q2-001 in detail.

What the lab's conclusion becomes. "Four null, one supported whose entry added nothing, one refusal" becomes, with a diagnostic beside it: "four null; one supported under the rules then in force, refused under ADR-0049/0050 on re-score; one refusal".

The lab-level finding stands and is strengthened: nothing found. The guards were looser than claimed, and that looseness biases toward false positives. So the nulls are robust and the single positive was the soft spot.


5. Sequenced task list

Every task ends with an acceptance check. make check (Makefile:39) must pass at the end of every task unless the task says a named test is expected RED until a named later task.

Phase 0 — governance, before any code (1 day)

TaskWhatDepends onAcceptance
OTL-T01Write ADR-0047 (G1–G6). Reserve 0048–0060 in docs/adr/README or the ADR index.—ADR merged; docs/adr/0047-*.md exists.
OTL-T02Add register/diagnostics.jsonl store class (Diagnostics(Store), record Rescored). Empty file committed.T01tests/test_register.py::test_diagnostics_store_accepts_only_rescored GREEN.
OTL-T03F11 part 1: register/HEADS.toml pinning today's six heads (P1 34 c2706a09716b…, P2 37 23cf22aa6586…, P3 14 e92fda14a7f1…, plus the three queues), with the test and prepublish check.—test_committed_registers_match_pinned_heads GREEN; the forged-chain and truncation tests GREEN.
OTL-T04F11 part 2: SSH signing key, allowed_signers, HEADS.toml.sig, CI verify step; ots stamp (optional tool, loud refusal when absent).T03CI "verify signed heads" green; .ots committed.

Phase 1 — make error rates measurable (2–3 days)

TaskWhatDepends onAcceptance
OTL-T05F23: occams/calibrate.py and tests/test_apparatus_size.py, with Probes A1–A3 as @slow tests. Expected RED: A1 at 0.01, A1 at 0.05, A2 at ρ 0.5, A3 (fifth check on the null control).T01Tests exist and fail with the probe numbers (±MC). The ρ = 0 control is GREEN.
OTL-T06F03: p + 1 and GuardEvidence for every check.T05test_mc_p_value_is_never_zero, test_resolution_records_evidence_for_passing_checks GREEN.
OTL-T07F20: extract occams/engine/probes.py, occams/engine/common.py, occams/inference.py with behaviour unchanged.T05All pre-existing tests GREEN; the probe numbers unchanged to the seed; test_survey_and_guard_share_fifth_check GREEN.
OTL-T08F13: occams/identity.py, closure sha, clean-code dirty check, EngineNotClean.T07The four F13 tests GREEN.

Phase 2 — P0 inference fixes (1–2 weeks)

TaskWhatDepends onAcceptance
OTL-T09ADR-0048. F01: winner-N resampling in position_boxed and the survey.T07F01 unit tests GREEN; A1 at 0.01 GREEN (≤ 0.026); A1 at 0.05 still RED (expected).
OTL-T10F02: dependent_null, calendar ordering, cluster-robust cross-check.T09A1 at 0.05, A2 at ρ 0.5 and A2 at ρ 0 all within tolerance; test_position_boxed_orders_blocks_by_calendar_day GREEN.
OTL-T11ADR-0049. F04: side-matched, dependence-aware fifth check with the selection/execution decomposition.T10test_fifth_check_refuses_coin_flip_on_drifting_paths GREEN; A3 GREEN; fifth-check power ≥ 0.65. make null --engine=day_boxed now lists beats-always-long among the refusals.
OTL-T12ADR-0050. F05 LCB floor and alternative_ev_net_r; F06 widest-cell σ and the winner-σ measurement refusal. Update controls.toml ([power] alternative_ev_net_r; [signal] planted at it).T10F05 and F06 tests GREEN; test_lcb_floor_coverage GREEN; make signal still ACCEPTED on both engines; --schema prints the new key with no value.
OTL-T13ADR-0051. F07 plateau.T10F07 tests GREEN.
OTL-T14Regenerate docs/console.html and docs/programme.html (make console, make programme) without changing any verdict. New evidence fields render as "not recorded (pre-ADR-0048)".T09–T13make prepublish clean; HEADS unchanged (the pages read the Registers, never write them).

Phase 3 — survey (1–2 weeks)

TaskWhatDepends onAcceptance
OTL-T15ADR-0052. F08 Romano–Wolf on the cell×date matrix, the screen/validate split inside definition, and the shrunk margin column.T10F08 tests GREEN, including test_readiness_on_a_null_grid_passes_nothing and the FWER and power slow tests.
OTL-T16F18 baseline-instability column and gate.T15test_readiness_flags_unstable_baseline GREEN.
OTL-T17F22 strings; README partition wording.—tests/test_docs.py GREEN.

Phase 4 — integrity (1 week)

TaskWhatDepends onAcceptance
OTL-T18ADR-0053. F09 structural overlap and data-set budget; stop auto-filling distinction.T01F09 tests GREEN.
OTL-T19F10 lab ceiling, lab ledger and the [lab].error_control key.T18F10 tests GREEN; the console shows lab spent/ceiling = 0.55/⟨author's ceiling⟩.
OTL-T20F12 locking and incremental verify.T03F12 tests GREEN; a loop over 1,000 appends is linear (timing test with a generous bound).
OTL-T21ADR-0055. F14 SOURCES.toml, snapshots and reproduce --at-source. Done in the private history repository for the six verdict trees.T08test_every_resolved_names_a_known_source GREEN.

Phase 5 — re-score the record under the conservative bundle (private, 1–2 days)

TaskWhatDepends onAcceptance
OTL-T22G4 step 1 for Q-003, Q-004, Q-005, Q2-001, Q2-002 (and Q3-001's REGISTERED→MEASURED refusal): exact reproduction at the stamped commit or source snapshot.T21Each prints an exact reproduction, or records Rescored(reproduced=False, reason).
OTL-T23G4 step 3 under ADR-0048 to 0051: one Rescored per question in register/diagnostics.jsonl; re-pin and re-sign HEADS.T22, T09–T13Six Rescored records. Q-003, Q-004, Q-005, Q2-002 and Q3-001 outcomes unchanged. Q2-001's diagnostic outcome recorded with every number (p, SE, LCB, winner σ, need_w).
OTL-T24Re-run the readiness computation for grid-001 and grid-002 under ADR-0052 from the recorded survey results (no new survey). Commit the new pages beside the old ones, labelled.T15, T16docs/surveys/grid-00{1,2}-…-readiness-adr0052.md exist; the old pages are untouched.
OTL-T25Append a dated, record-derived note to docs/LAB-CONCLUSION.md. Write a new section; do not rewrite the adopted text. It quotes the Rescored records.T23, T24The note cites diagnostics seq numbers; make prepublish clean.

Phase 6 — realism, non-conservative (weeks; data-dependent)

TaskWhatDepends onAcceptance
OTL-T26ADR-0057. F16 stop slippage (required key) and cost curve; M5.0 reopened. Measure spreads with provenance.T23F16 tests GREEN; costs.basis="measured" available.
OTL-T27ADR-0056. F15 point-in-time membership, or the survivorship diagnostic for sp_100 and dow_30.T23F15 tests GREEN; a SurvivorshipDiagnostic for each single-name universe.
OTL-T28ADR-0058. F17 ApparatusCalibrated gate.T05, T12test_measure_refuses_without_calibration_for_this_code GREEN.
OTL-T29Second G3 re-score of Q2-001 and Q2-002 with F15 and F16 on top of the conservative bundle.T26, T27Two more Rescored records, each naming its full ADR set.

Phase 7 — hygiene (3–4 days, can run in parallel from Phase 1)

TaskWhatDepends onAcceptance
OTL-T30F19 test_core_usage, test_check_never_imports_boto3.—GREEN.
OTL-T31ADR-0059. F21 entry registry and causality property test.T07F21 tests GREEN; docs/EXTENDING.md §3 rewritten to the registry path.
OTL-T32F24 ruff widening, strict zips, leave-one-out refusal on non-positive pooled score, duplicate import.—make lint GREEN under the new selection; F24 tests GREEN.
OTL-T33F25 TASKS split and CLAUDE.md ≤ 5 KB.—test_claude_md_is_small, test_tasks_has_no_status_log GREEN.

Definition of done

  1. ADR-0047 to ADR-0059 are adopted. Each names the fixes it covers and binds from the next registration.
  2. Every test named in §3 exists. Every test marked RED was observed failing on 2e86d58 and now passes. make check is green locally and in CI, and the nightly -m slow job is green.
  3. The apparatus size and power table (F23) shows, on both engines, for S ≥ 200 seeds: - beats-null size ≤ α + tol at α ∈ {0.01, 0.05} for ρ ∈ {0, 0.5}; - fifth-check size ≤ α + tol on a drifting world; - LCB-floor coverage ≥ 1 − α_c − tol; - power ≥ planned − tol at the declared alternative.

ApparatusCalibrated is recorded for the current engine_code_sha.

  1. make null is refused by beats-null and beats-always-long on the day-boxed engine, not by the floor alone. make signal is accepted at the declared alternative.
  2. register/HEADS.toml is committed, signed and OpenTimestamps-anchored. Forgery and truncation tests are green, and prepublish checks the heads.
  3. No measurement can run on dirty or unknown code. Every new HypothesisMeasured, HypothesisResolved and SurveyRecorded carries engine_code_sha over the import closure.
  4. register/diagnostics.jsonl holds one Rescored per historical question, each first reproduced exactly. No line of the three programme Registers has changed, which the heads pinned in T03 prove.
  5. The readiness tables for grid-001 and grid-002 exist under ADR-0052 beside the originals. The README says "definition partition (30 %)" and "measurement partition (50 %)", and never "half".
  6. CLAUDE.md ≤ 5 KB, and TASKS-v4.md holds the plan without the diary.

Appendix A — probes (run against 2e86d58)

All probes ran in a scratch venv (pip install -e ".[dev]", Python 3.12.10), offline, without touching the repository. They are reproduced here so that OTL-T05 can turn them into tests.

A1 — position_boxed null size (F01, F02, F04). 200 seeds, about 150 s.

world = {n: random_walk(n, days=1260, seed=seed, sigma_daily=0.02, drift_daily=-0.02**2/2) for n in "ABCDEFGH"}
t = StrategySpec(entries=(Entry(EntryKind.DOWN_RUN, Side.LONG, (("runs", 3),)),), exits=(Exit(ExitKind.TIME, (("bars", 5),)),),
                 stop=Stop(StopKind.PERCENT, 10.0), sizing=Sizing(), order_type=OrderType.MARKET, horizon=Horizon.MULTI_DAY,
                 universe=UniverseRule((("world", "probe"),)),
                 required_capabilities=frozenset({Capability.NATIVE_STOP, Capability.RESTING_ORDERS}), axis=InformationAxis.PRICE_DAILY)
m = position_boxed.measure(t, {"hold": [5.0]}, lambda t, p: t, world, seed=seed, cost_in_r=0.0, null_draws=2000)
p = (1 + sum(v >= m.winner.ev for v in m.null_ev)) / (1 + len(m.null_ev))
# "fixed-N": the same random-entry positions, moving-block resampled to length m.winner.n

Result: winner N ≈ 645, random-entry pool ≈ 2,035. beats-null false-positive rate: 0.140 at 0.01 and 0.225 at 0.05. Fixed-N: 0.015 and 0.100. beats-always-long: 0.065 and 0.135.

A2 — day_boxed null under a common factor (F02). 10 names, 1,260 days. Daily return = −σ²/2 + σ(√ρ·z_common + √(1−ρ)·z_name), σ 0.02. down_run(2), intraday, time exit 1 bar, stop 5 %, zero cost, single cell, 200 seeds, about 300 s each.

ρfalse positives at 0.01at 0.05winner Ndistinct dates
0.00.0100.050about 3,192about 1,189
0.50.0900.200about 3,185about 917

A3 — fifth check on the day-boxed null control (F04). controls._day_boxed_measurement("null", load_controls(), seed=20260910, …).

A4 — Register forgery and truncation (F11).

**A5 — numbers used in §4 (scipy, occams.core.power).**