# CNT-1 — the go-live gate progress PUBLISHED on thales.report is one ahead of what the gate's own evaluator computes

status: open · raised 2026-08-27 (interactive session, while reviewing PR #115) · class: truth-divergence-between-layers (reporting only; the gate itself is correct) · judgement: **YES** · effort ~0.25 pd · horizon: live now on the public site; matters most at the December gate

---

**Plain-language summary for an owner reading one paragraph.** Three places
count "how far into the clean clock are we." The one that DECIDES —
`go_live.py` — is right. The two the owner and the public READ are both one
day ahead: the daily digest says *forward OOS day 53/60* and `gate.json`
publishes *Clean live history 53 of 126*, while the gate's own evaluator
computes **52**. Nobody makes a decision off the two that are wrong, so the
consequence today is small — but the site's entire premise is that every
number is authored by the CLI, and this is a published number that disagrees
with the criterion it claims to report. It is also the *same* marks-vs-returns
confusion the 2026-08-17 weekly audit already had to correct once, when a
hand-count mis-dated the oos-monitor maturity by four days.

## Mechanism — verified line-by-line at HEAD `2072f4b`

Two counting conventions, three call sites:

| site | counts | value at the 2026-08-26 mark |
|---|---|---|
| `backtest/oos_monitor.py:185` `load_live_returns` | **returns** | **52** |
| `execution/go_live.py:234` `n_live = int(live.size)` | returns (calls the loader) | **52** ✅ decides |
| `execution/digest.py:127` `sum(1 for e in equity_hist if e["date"] >= oos_since)` | **marks** | 53 ❌ owner reads daily |
| `execution/public_export.py:159` `"clean_trading_days": len(clean)` → `gate.json` `progress.have` (`:312`) | **marks** | 53 ❌ published |

`load_live_returns`' own docstring states the cause: *"One return is lost at
the boundary (it needs the prior day's equity), which is correct."* A daily
return is a difference between two marks, so N marks give N−1 returns. Both
reporting sites count marks with an inclusive `>= since` filter and label the
result as though it were the gate's unit.

Confirmed empirically, not by inspection alone: the loader returns 52 against
committed state at the 08-26 mark, while `momentum.json`/`gate.json` published
`clean_trading_days: 53` the same day.

## Why the obvious fix is WRONG

**Do not subtract one.** The gap is not a fixed offset:
`load_live_returns(trim_leading_flat=True)` also trims leading flat days
(warm-up, not performance), so the marks count is an **upper bound** on the
returns count, not `returns + 1`. It happens to be exactly one today because
there are no leading flats after 2026-06-11. Hardcoding `-1` would be another
hand-count — precisely the error that produced the 2026-09-04 mis-dating the
08-17 audit had to fix.

The honest fix is that a surface reporting the gate's progress must call the
**same function the gate calls**, or must label its own number as marks and
stop calling it gate progress.

## Data plan

None — owned committed state plus static code. Reproduce with:

```
python -c "from thales.backtest.oos_monitor import load_live_returns; \
print(load_live_returns(state_dir='data/processed', since='2026-06-11').size)"
```

## Test design + kill criterion (pre-registered, negative-controlled)

1. On a fixture equity history with a known clean-clock boundary, assert the
   digest's `live_oos_days` **equals** `load_live_returns(...).size` — deny-
   asserted (red today).
2. Same assertion for `public_export.sleeve_stats(...)["clean_trading_days"]`
   as consumed by `gate.json` `progress.have`.
3. **The leading-flat negative control, which is the whole point:** a fixture
   carrying two leading flat days after `since` must still agree. A `-1`
   implementation passes tests 1–2 and **fails this one**, so the test suite
   distinguishes the correct fix from the tempting one.

**Kill criterion:** all three green post-fix; revert the change and tests 1–2
red. If test 3 cannot be made to pass without duplicating the loader's
trimming logic, that is itself the finding — it means the surfaces should
call the loader rather than reimplement it, which is the preferred fix shape
anyway.

## Fix shape (~0.25 pd, PROPOSE-only — this row does not implement it)

Preferred: both surfaces call `load_live_returns` and report its `.size`, so
there is one counter and it is the one that decides.

Fallback if the digest must stay loader-free (it is observability and must
never import a decision path that could raise): keep the marks count but
**rename it** — "53 clean marks" is true; "forward OOS day 53/60" is not,
because `/60` is the loader's threshold.

## Scope note

`vrp.json`'s `forward gate day 33/126` is a DIFFERENT clock (the VRP 126-td
forward gate, `fleet_digest.py:788`) and is not covered by this row; whether
it shares the convention was not checked. Left explicit rather than silently
assumed.

---

## Triage note — 2026-08-31 (annotation only; the body above is untouched)

**This row acquires a hard date it does not currently state: 2026-09-04.** It
was filed as a reporting divergence that "matters most at the December gate".
That understates it — the off-by-one produces a **false maturity announcement
on the owner's only daily push channel, four trading days from now, in the
week the 60-day clocks actually mature, during a probable owner absence.**

Verified at HEAD `528793d`, both counters reproduced against committed state
at the 2026-08-28 mark — **55 marks vs 54 returns**:

- decides: `oos_monitor.load_live_returns` and `go_live.py` count **returns**;
- reports: `digest.py` and `public_export.py` count **marks**;
- published today: `web/public/data/gate.json` carries `progress.have: 55`
  against a true 54.

**The dated consequence.** The marks counter reaches 60 on **2026-09-04**,
while the judge is still four days and one holiday short (the returns counter
reaches 60 on **2026-09-08**, independently recomputed this run and matching
`NORTHSTAR.md` §6 exactly). At that point `digest.py`'s `if n_oos <
OOS_MIN_DAYS` branch flips: the daily email stops saying "verdict pending" and
begins reporting the window as complete. From 09-08 `fleet_digest.py` prints
the raw counter against a hardcoded `/60` with **no branch at all**, so the
email will read "forward OOS day 61/60", "62/60".

**And `digest.py` carries an active false reassurance about exactly this.**
Its import line is annotated `# one truth` — true of the *threshold*
(`MIN_OOS_DAYS` is shared), false of the *counter*, which is the half that is
wrong. A maintainer reading that comment concludes the divergence cannot exist.

**Ranking consequence:** this moves from "cosmetic, December" to the cheapest
dated item in the queue — 0.25 pd against a consequence nine days out on the
channel the owner reads when he is not at his desk.

---

## BUILT — 2026-09-02

**Outcome: shipped, the honest fix.** One function — `oos_monitor.live_returns_from_marks`
(the pure core `load_live_returns` now calls) plus `live_return_count` — and
every progress surface calls it: the daily digest's breadcrumb, the fleet
email (which also gains a matured branch instead of "61/60" against a
hardcoded `/60`), and `public_export.sleeve_stats` / `gate.json`'s
`progress.have`. No `-1`, no hand-count: marks minus trimmed leading flats
minus one, computed by the judge's own code.

**Route — recorded honestly:** built on the owner's direct instruction
(interactive, 2026-09-02, "let's do it" on the post-audit pending list),
NOT via `queue/approved/`; a human merge.

**Kill criterion — met:** the lockstep test builds a history with two
leading flats after the clock start (4 marks, 2 returns) and asserts the
site's `clean_trading_days` equals `load_live_returns(...).size`; the two
existing marks-count assertions were changed to the judge's unit (2 marks →
1), which is exactly the one-day-ahead defect the row describes. The digest
line that would have announced "60/60" on 2026-09-04 now reads 59/60 that day
and matures on 09-08 with the judge.

---

*Provenance recorded by triage 2026-09-07 (annotation only; the record above is untouched): shipped in **PR #141** (`b50ae9b`, merged 2026-09-03).*
