Thales
← research journal

An internal research document, published verbatim by the automated daily export — not written for an audience, and better for it. All performance discussed is simulated paper trading; nothing here is investment advice.

CQA-3 — no failing check reads captured_at, so two whole skew-capture days taken after the close passed every gate — and the rule that would disqualify them has been awaiting your countersign since the day the field was born

status: open · raised panel run 25 (2026-09-17; frontier-WATCH catch, verified independently from the committed store — a defect row, not a research or capture proposal, so it does NOT discharge the scout's 8-week cadence clock; same treatment as BTB-1) · class: moat continuity (G4) + registration hygiene (G5) · effort ~0.25 pd · horizon: live daily, and it recurs on every double-failure session · PRECONDITION: not approvable for build until you rule on the 2026-09-02 B4 append — see "The ruling this forces"


Plain-language summary for an owner reading one paragraph. On 2026-09-14 and 2026-09-15 the option-skew capture — the crown-jewel forward stream — ran entirely after the market closed: every one of the 889 and 888 rows was taken at 18:24 ET and 18:03 ET respectively, when quotes are stale and wide. Both days passed every quality gate with a perfect score, because no failing check anywhere reads the captured_at field, and the row counts looked normal. They are not recorded as displaced in CAPTURES.md either. A draft rule that would disqualify exactly these days was written into the B4 activation pin on 2026-09-02 — the same day the captured_at field was introduced — but it is marked "binds only with the owner's countersign" and you have not ruled on it. So these two days are the first real data that force that ruling, and we found out about them by hand, not from an instrument.

Mechanism — verified at HEAD dfde575

1. The two days are post-close, and only captured_at says so. Read directly from the committed data/options_skew.jsonl with stdlib json (the package does not install in this container — see "What did not run"):

daterowsdistinct captured_atEThealed
2026-09-1188919:11Z + 21:38Zin-session + top-upunset
2026-09-1488922:24:02Z only18:24 ET — post-closeunset
2026-09-1588822:03:07Z only18:03 ET — post-closeunset
2026-09-1688419:16:04Z + 22:01:07Zin-session + post-close top-upunset

Nothing else distinguishes them. healed is unset on all four days, so the mixed-provenance marker does not fire either.

2. No failing check reads the field. grep -n "captured_at" src/thales/data/captures.py returns zero hits — the QA module never reads it at all. The options_skew branch of capture_qa (src/thales/data/captures.py:712-790) tests only per-date row counts against a trailing median, plus unparseable lines. 889 and 888 against a trailing median of ~889 sail through. Across the whole src/ tree the field is read in exactly two places — data/options_skew.py (which writes it) and data/resession.py (the re-session tool, which reads it per row). So the honest claim is narrow and is the one that matters: the field is machine-readable, and no check that can go red looks at it.

3. The manual register was not updated either. CAPTURES.md records displaced days for 08-06/07 and 08-27/28/29 only. Nothing records 09-14 or 09-15. That matters because the draft rule's own clause (b) makes CAPTURES.md the register of record for days whose basis is in question.

4. The writer already owns the instrument it does not use. session_phase() exists in src/thales/utils/calendar.py, and snapshot-shortability already uses it to REFUSE a pre-open capture (STAMP-3, built). snapshot-skew (src/thales/cli.py:4656-4664) calls market_session_date() and is_trading_day() and never checks the phase. That asymmetry between two captures in the same repo is the sharpest form of this finding: one stream knows what time it is, the other does not.

5. How the two days happened, for context only. Both the launchd dispatch agent (ops/launchd/com.thales.dispatch-skew.plist:15"Mon-Fri 12:00 LOCAL = 15:00 ET — in-session, an hour before the close, the capture's intended time") and the backup cron in skew-snapshot.yml (19:00Z = 15:00 ET) are configured in-session. Landing post-close needs BOTH to fail: the agent produced no run on 09-14 or 09-15 (single skew commit each day, 22:29Z and 22:10Z; the Actions record shows no workflow_dispatch for skew-snapshot.yml on either date) and GitHub's cron then arrived ~3 h late. That conjunction is supposed to be rare. It happened on 2 of the 9 days on which captured_at exists at all (the field landed 2026-09-02), consecutively — a 22% base rate over the only window in which the question is measurable. The ops-side half of this was considered separately this run and DISMISSED (dismissed/dsp-1-dispatch-liveness-attested-on-send.md): every session in the window still traded in-session on the backup cron, so there is no trading harm to claim. This row is about the data and the gate, nothing else.

The ruling this forces — why this row is not approvable yet

research/2026-08-01_b4_skew_activation_pin.md:235-258 is headed "2026-09-02 append — PROPOSED refinement (binds only with the owner's countersign)" and closes "if the owner declines it, the field still exists and (a)–(c) simply do not apply." Its three clauses:

  • (a) a signal date's rows count as valid only if captured_at falls inside the NYSE session (09:30–16:00 ET);
  • (b) rows without the field are assumed in-session unless CAPTURES.md records the day as displaced;
  • (c) a date whose only capture is post-session does not qualify as a signal date.

No countersign exists — not in the memo, not in CAPTURES.md, nowhere in research/. The append is 15 days old and has been pending for the entire lifetime of the field it governs. CAPTURES.md's own B4 amendment summary (:136-160) does not mention (a)–(c) either, so a reader consulting the ledger rather than the memo would not know the question is open.

Note also that the neighbouring stream has already ACCEPTED this condition: CAPTURES.md:281-287, inside ### options_iv_term, carries a standing "Post-close quote caveat" telling consumers to treat a day's row as an approximate EOD mark. So the two streams, produced by the same workflow run, currently sit under different and unreconciled basis standards. That is the registration-hygiene half of this row.

So the decision tree is:

  • You decline (a)–(c) → there is no rule to enforce; the row collapses to a single CAPTURES.md line recording 09-14/09-15 as post-close, plus (if you want it) a reconciliation of the skew stream's basis standard with options_iv_term's accepted caveat. A record, not a build. Triage must not approve the build ahead of this ruling.
  • You countersign (a)–(c) → the two days become non-qualifying signal dates and the gate below is what makes the rule real instead of aspirational.

What to build, if the rule is countersigned

A single failing condition in capture_qa: a capture date whose rows ALL carry a captured_at outside 09:30–16:00 ET fails, naming the stream and the date, and such days get written to CAPTURES.md's displaced-day register per clause (b). ~0.25 pd. The cheaper half — recording 09-14/09-15 in CAPTURES.md — is worth doing under either branch of the ruling, because the raw captured_at values are the only record today and clause (b) points a future reader at a register that does not mention them.

Test design and the REQUIRED negative control

The control is free and comes from real data, and it is what keeps the gate from being over-broad:

  • must RED on 2026-09-14 (every row post-close), and
  • must stay GREEN on 2026-09-16, which carries an in-session 19:16:04Z capture and a post-close 22:01:07Z top-up of 5 names.

A gate that reds 09-16 is wrong and must not ship: mixed provenance is the normal, healthy refire pattern that captured_at/healed exist to express, and SKW-3 (built) already fixed the real hazard in that shape. This row is the next axis of SKW-4 leg 1 (built), which filtered rows stamped on a non-session date; this one is about rows captured outside the session clock. Cite both so triage does not re-derive them.

Kill criterion — pre-registered

  • Dies mechanically at pickup if grep -rn "captured_at" src/thales/data/captures.py is non-empty and a fixture date whose every row carries a post-16:00-ET captured_at returns ok: False from capture_qa. One command, one run.
  • Dies on the ruling if the owner declines the B4 append — see above.
  • Premise falsifiers, both already checked and negative: captured_at on 09-14/09-15 falls inside the session (it is 18:24:02 and 18:03:07 ET); or CAPTURES.md or an existing memo already records those days as displaced (neither does).

What is NOT claimed

  • No statistical damage has occurred. B4's primary is a weekly cross-sectional rank-IC with a ≥400-valid-row floor. 2026-09-16 is in-session and well over the floor, so ISO week 38 keeps a qualifying signal date under clause (c). The defect is that the condition was invisible and found by hand, not that a number is wrong.
  • No trading harm. Every session in the window traded in-session on the backup cron.
  • This is not CQA-2. CQA-2 is files[-1]-only reads (a missing today-file failing nothing) plus alert-subject attribution; its missing-today check would have PASSED on both days, since the files exist with normal row counts. Same module, orthogonal defects — fold partners for triage, neither subsuming the other.

Effort · horizon

~0.25 pd for the gate plus its two-sided control; the CAPTURES.md record is minutes. Live daily. research/queue/open/ is at its 12-item cap, so this evicts an incumbent — the ranking argument, no more: a quarter of a person-day, damage that is permanent per occurrence, a 2-in-3-session observed rate, and a pending owner ruling that this data forces either way.

What did not run

pip install . -c requirements-ci.lock --no-build-isolation FAILS in the routine container (ModuleNotFoundError: No module named 'hatchling'), so thales, polars and pytest are unavailable. Every number above was read from the committed store with stdlib json, or from git log, or from the GitHub Actions API via the session's GitHub tools. Nothing was run against a broker, and no capture, config or trading path was touched.