# SAC-1 — census of trading-path persist sites: "state advanced or keyed without regard to run completion" (member four already found)

status: open · raised panel run 15 (2026-08-31), answering the owner handoff in
60a6630 / RESEARCH.md's top banner ("sweep for member four instead of waiting
for it") · class: loudness & safety (live path) · judgement: **YES** · effort
~0.5–1 pd · horizon: before momentum's next halted-selection retry sequence
(the 08-19/20 meanrev sequence puts the trigger cadence at roughly monthly)

---

**Plain-language summary for an owner reading one paragraph.** The last month
produced three separately-discovered defects with one shape: the system wrote
down what it *intended* to do — or what day it believed it was — without
checking that the run actually completed. RWG-1 leg 2 (the turnover-blend base
advanced during a safety halt), STAMP-1/STAMP-2 (a wall-clock date consumed
the next session's idempotency slot), and the Kelly double-booking fixed in
PR #120. You asked the panel to sweep for member four instead of waiting for
it to fire. The sweep found it in under an hour — which is the evidence the
class is productive and a one-shot census is worth a row.

## Member four (verified this run; recorded here so the census consumes it rather than rediscovers it)

`src/thales/execution/daily.py:821-831` persists `last_construction`
(including the intended `vol_scalar`) inside `_size_targets`, gated only on
`dry_run`. `_size_targets` runs at `daily.py:1638`; `execute_orders` at
`:1719` — the persist precedes the safety gate. Consumption chain:
`daily.py:658-662` reads `last_construction.vol_scalar` into every subsequent
daily portfolio snapshot, and `_gross_returns` (`:714-721`) divides each
realized return by it for the vol-target estimator. So a selection that sizes,
persists the scalar, then safety-HALTs leaves the estimator attributing the
real (old-scalar) book's returns to a scalar that was never applied — for up
to a month on momentum, which sizes from state this corrupts.

## Mechanism — the census

One-shot enumeration of every persist site in the trading path (`daily.py`,
`vrp_daily.py`, `pipeline.py`, `state.py`, `tca.py`), each classified on two
axes:

- **(a)** gated on run completion (halt/exception) vs ungated;
- **(b)** keyed on the unit of work (period/session) vs wall-clock.

For each ungated site, a ruling: **deliberate** (fact-recording/telemetry —
e.g. `append_equity`, portfolio snapshots record what IS, regardless of what
executed) vs **defect** (intent persisted as if executed — e.g.
`last_construction`, the class members above). Deliverable: the classification
table as a memo in `research/`. Propose-only; no gate ships from this row.

## Scope boundary (C0 — stated so this cannot collide with existing rows)

- **Trading-path persist sites only.** The capture writers are
  STAMP-1/STAMP-2's owned territory and are out of scope here.
- Per-site rulings for `last_targets` and the kill-switch Kelly-snapshot wipe
  **defer to RWG-1** (legs 2–3), which owns them. The census enumerates and
  classifies; it does not re-litigate a row that already owns a site.

## Data plan

Static code reading, cross-checked against the committed natural experiment:
for each site, diff its persisted value across meanrev's 08-19/20 halted runs
and the 08-21 recovery (the same commits that confirmed the Kelly
double-booking). Observed advancement during a halt is ground truth, not
inference. Owned; zero external data; zero registry rows.

## Test design

For every site the owner rules "must be gated": a halt-injection test — force
a safety HALT, assert the site's persisted state is unchanged — with a
negative control per gate (revert the gate; only its own test reds). Sites
ruled deliberately ungated get a pinned comment naming the ruling, not a test.

## Kill criterion (pre-registered)

If the census finds **zero** ungated-defect sites beyond the already-recorded
members (RWG-1 legs 2/3) plus `last_construction` (named above), the class is
declared fully enumerated, the row closes with the table as its record, and
the class may not be reopened without a new fired instance.

## Cap note for triage

`queue/open/` is at/over the 12-cap. Rank this against the tree explicitly
rather than letting it slide in — if something must yield, the natural
fold-partner is the STAMP-1+STAMP-2 shared fix-sitting (same family), never
RWG-1 (deliberately narrowed).

---

## Dated note — 2026-09-05 (maintainer, from the external design review r2 verification)

Two more sites for the census, both verified at `cd571b2`, neither owned by
another row at the time of writing:

- **`_record_safety_event` arms the same-day idempotency gate on a HALT.**
  `pipeline.py:274` writes a `run_summary` for any safety HALT;
  `already_ran_today` (`state.py:180-187`) honours *any* `run_summary` for
  the date; `daily.py:1392` then skips with `"Already ran today"`, which is
  not in `cli.py:1107`'s `critical_skips`, so the run exits 0. RUNBOOK.md's
  post-halt instruction ("fix the cause, then `thales run --skip-market-check`
  locally", :253-254) is therefore a green no-op without `--force` — which
  the same sentence says never to use casually. Axis (a): ungated — a HALT is
  recorded as if the day's unit of work completed. Ruling needed: *is a HALT
  a completed unit of work for idempotency purposes?* Damage is bounded to
  one lost day because the month stays open via `selection_completed=False`
  (`daily.py:502-511`), which is why the reviewer filed it as a near-miss
  rather than a finding.
- **Kill-switch `active: True` persisted before the liquidation is attempted**
  (`daily.py:1024-1026`, then `_liquidate_to_cash` → `execute_orders` at
  `:1133`). Owned by **KSP-1** (raised the same day) for the fix; listed here
  so the census consumes it rather than rediscovers it. Same class: intent
  written as fact.

Neither changes this row's kill criterion; both count as members beyond the
already-recorded ones, so the criterion's zero-branch cannot fire on the
current tree.

---

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

**Two of the five named members are CLOSED by KSP-1 (#151, `47ea024`); three
stand, and they are the three that corrupt sizing state.** Verified at HEAD
`e25f80b`:

| member | status |
|---|---|
| `last_construction` / `vol_scalar` (member four) | **live** — `daily.py:841-849`, gated only on `dry_run` (`:840`); `_size_targets` at `:1781`, `execute_orders` at `:1862` |
| `_record_safety_event` arms same-day idempotency | **live** — `pipeline.py:274` writes a `run_summary` on any HALT; `state.already_ran_today` honours any of them (`state.py:180-187`); `daily.py:1523` then skips, and `"Already ran today"` is not in `cli.py:1107`'s `critical_skips`, so the run exits 0 |
| kill-switch `active: True` before liquidation | **CLOSED** (KSP-1: deferred publish, `daily.py:1103-1106` → `:1227-1230`) |
| Kelly-snapshot wipe on a halted liquidation | **CLOSED** (KSP-1: non-halt branch only, `daily.py:1231-1234`) |
| `last_targets` (deferred to RWG-1) | **live** — `daily.py:1795-1803` |

**The kill criterion's zero-branch therefore cannot fire**, exactly as the
09-05 dated note anticipated. STAMP-1/2/3 closed the *wall-clock keying* axis
for the two run wrappers; they closed none of the persist sites above, which
sit on axis (a).

**The deliverable does not exist yet**: no census memo among
`research/2026-09-*`; a grep of `research/` for SAC-1 returns only queue files
and the two design-review memos. Triage carries this into the decide-list
folded with RWG-1 legs 2+3, per this row's own sequencing request — one
ruling sitting, not two.
