Post-review money-path correctness fix pass on feat/down-side — the branch implementing the both-sides plan (adds a DOWN trading side to the LIVE UP-only Polymarket 5m BTC producer). Headline is a persist-before-publish dedup invariant. Gates green; NOT pushed, NOT deployed, no live-DB writes.

For Agents

Repo wowjeeez/pmv2-crypto-algo @ /Users/levander/coding/pmv2/crypto_algo, branch feat/down-side, 3 commits (0d5324f ingest warn-logging, 1aef0ef probe_window, 90808d0 observe core) — NOT pushed/deployed. C1 invariant: the emitted_signals INSERT (ON CONFLICT (window_id) DO NOTHING) runs BEFORE the NATS publish — the written DB row is the dedup authority. Decision is a pure helper fire_decision(Option<u64>) -> {Publish|SkipDuplicate|SkipError}. Never a duplicate real-money trade; worst case is a detectable missed trade. Observation now carries up: BookView, down: BookView (not 16 flat fields); per-side selection via pure fire_params(side, obs, market). FireSide deleted → select_side(p_cal) -> Option<Side> (≥0.80 Up, ≤0.20 Down). Regime gate FROZEN: τ∈[60,150)s ∧ |p_cal−0.5|≥0.30. Startup fail-fast probe_schema_column requires migration 0006. Tooling: run clippy/fmt via rtk proxy (the rtk hook mangles -- -D warnings).

C1 — Persist-before-publish (the money-path invariant)

The most important change. emit.rs runs the emitted_signals INSERT (ON CONFLICT (window_id) DO NOTHING) before publishing to NATS. The written DB row — not an in-memory set — is the dedup authority.

The branch logic is a pure, unit-tested helper on the INSERT’s rows_affected:

fire_decision(Option<u64>) -> FireDecision   // { Publish, SkipDuplicate, SkipError }
  None      (INSERT errored)        => SkipError      // log + NO publish; retry next tick
  Some(0)   (window already claimed) => SkipDuplicate  // NO publish
  Some(1)   (claim owned)            => Publish

Invariant: never a duplicate real-money trade

A publish failure that happens after a successful claim is logged loudly and never retried — the written row is the detectable record of a MISSED trade. Worst case is therefore a detectable missed trade, never a duplicate spend.

Conservative spend accounting (deliberate)

In-memory fired_windows / total_fires are advanced right after the successful claim, before the publish attempt — so a hung or failed publish can never re-attempt the same window. Spend is over-counted (never under-counted) by construction.

Supersedes the earlier publish-then-mark ordering

C1 inverts the prior rule from commit bc38734 (crypto-shortterm-emit-guardrails, “mark fired only after publish AND db-insert both succeed, else retry next tick”). The trade-off moved from “never lose a signal” (retry on publish failure, but risks duplicate spend on a deploy/crash) to “never duplicate real money” (claim first; a post-claim publish failure is an accepted, detectable missed trade).

BookView refactor — kills the “Down-reads-UP-book” bug class

  • observe::Observation now carries up: BookView, down: BookView (8 fields each) instead of 16 flat pm_* / down_* fields.
  • book_cols() returns a BookView — this killed the positional 8-tuple BookCols transposition hazard (a mis-ordered tuple silently swapping columns).
  • emit selects per-side params via a pure, unit-tested fire_params(side, obs, market) — this closes the copy-paste “Down reads the UP book” bug class. A test asserts Down ⇒ down book / token_down / down_outcome_index / SOURCE_DOWN / "Down".

The recorder/SQL boundary stays FLAT

recorder::Row::Observation stays flat; BookView is mapped to flat fields in observe/src/main.rs. Do NOT push BookView across the recorder/SQL boundary — the struct grouping is an in-engine convenience only.

select_side — FireSide deleted

  • The FireSide enum is deleted.
  • Replaced by select_side(p_cal) -> Option<types::Side>, simplified to two threshold arms: >= 0.80 → Up, <= 0.20 → Down, else None.
  • The regime gate is unchanged and frozen: τ ∈ [60,150)s AND |p_cal − 0.5| ≥ 0.30 (so a fire requires p_cal ≥ 0.80 or ≤ 0.20 by construction).

C2 — startup fail-fast schema probe

  • observe::probe_schema_column(pool, table, col) aborts startup with a clear “apply migration 0006” error when a required column is missing — instead of letting every INSERT fail inside the hot loop.
  • emit probes emitted_signals.source; observe probes observations.pm_down_ask.

Test-integrity / DRY

  • is_regime() and max_price() are now shared between emit.rs and the lib tests — so a test can no longer pass while the real gate regresses (previously the tests had hand-copied reimplementations).
  • Deleted stale UP-only tautology tests (up_only_fires_p_cal_above_half, down_skipped_p_cal_below_half) and the hand-copied is_candidate_test / compute_max_price.

Down-book error visibility

  • ingest poll_book’s three error arms now log at warn! (were debug!, which is suppressed at prod RUST_LOG=info) with side + token — so a Down-book outage is observable in production instead of silent.

probe_window diagnostic

  • The diagnostic now treats source=crypto_5m_algo_down rows as expected: they store p_cal = P(up) next to the Down ask, which legitimately differs (it previously looked like a mismatch).
  • The p_cal column meaning is unchanged — it is always P(up), for both sides.

Tooling gotcha — rtk mangles clippy/fmt flags

Run clippy & fmt via rtk proxy

The rtk cargo hook mangles cargo clippy --all-targets -- -D warnings — it passes -D to rustc as a filename, producing a "multiple input filenames" error. Use:

rtk proxy cargo clippy --all-targets -- -D warnings
rtk proxy cargo fmt --check

cargo fmt --check is affected the same way — also run it via rtk proxy.

State

  • 3 commits on feat/down-side: 0d5324f (ingest warn-logging), 1aef0ef (probe_window), 90808d0 (observe core).
  • Whole-workspace gates green: build clean, 84 tests passed (3 ignored live-smoke), clippy -D warnings clean, fmt --check clean.
  • NOT pushed, NOT deployed, no live-DB writes.
  • This is the post-review fix pass on top of the both-sides feature work.