Skip to content

report sink: read trades and equity in one raw sequential pass each - #529

Merged
luisleo526 merged 25 commits into
mainfrom
report-sink-equity-scan
Oct 26, 2026
Merged

luisleo526 merged 25 commits into
mainfrom
report-sink-equity-scan

Conversation

@luisleo526

@luisleo526 luisleo526 commented Oct 26, 2026 •

Copy link
Copy Markdown
Collaborator

What this changes

The run wrapper writes its report from a verified sink file. After the strict verification pass (header, every record, trailer, counts, digest, run id and receipt, unchanged and still required), the trade records and the equity records were each read back through the strict reader a second time. Both are now read in one fresh raw sequential stream, sharing one scan:

  • one unbuffered read of the sink in 64 KiB windows, no seek and no line reader;
  • the SHA-256 of every byte before the trailer is computed from the same chunks that are cut into lines and decoded, so what is decoded is exactly what is hashed;
  • trade lines are decoded with RecordParser.parse; equity lines with RecordParser.parse_verified, which decodes after the strict pass has verified the sink: same framing refusals (line feed, 1 MiB cap, blank line, byte-order mark), one json.loads with the shared token hooks, and the shared field coercion (exact integer time in the 53-bit range, optional finite double whose text equals repr, -0.0 kept). It does not repeat the strict parser's byte-by-byte lexical walk;
  • other record bodies are skipped without being walked or retained, including lines whose tag is written with a JSON escape;
  • needle search keeps a 5-byte carry across window edges (the longest needle is \n["T",, 6 bytes); one pending line is held at a time, capped at 1 MiB including the LF; the scan loop is one function, _tagged_records(tag), for both tags;
  • at exhaustion: framing, trailer line feed and end of file are checked first, then the digest against the verified sink's digest, then the record count against the verified count. An inode/size mismatch at open is refused early. Read and open errors are typed refusals, never reported as a file change.

Every other records(tag), and records() with no tag, still uses the strict reader.

Behaviour

  • Report output is byte-identical (frozen-bytes test on a real-sink corpus subset with equity arrays).
  • Memory stays flat in bars and trades: one 64 KiB window, a 5-byte carry, one pending line.
  • A changed byte after verification is refused with the report sink changed after verification: digest differs for both trades and equity. For equity this replaces the strict parser's text; a tag mismatch in the verified decoder says record tag is 'T', expected 'E'.
  • No speed figure is claimed here: only a four-build measurement on a quiet box can support one.

Tests

New docker/run_json_trade_scan_test.py and docker/run_json_equity_scan_test.py (real sinks, with the independent strict reader, RecordParser.parse and the frozen writer as oracles): window-edge sweeps for literal, escaped and trailer needles at every offset around the 64 KiB boundary, carry mutants, decoder equality with the strict records (types, float bits, serialized bytes, every accepted repr spelling and its refused alternates), typed hostile-byte refusals, one raw stream per scan, same-chunk hashing with injected read flips, identity/digest/count ordering, early close, I/O errors, publication cleanup. Existing rows in tests/test_run_json_stream_fastpath.py, docker/run_json_bounded_transport_test.py and tests/test_run_strategy_sink.py that pinned the old strict passes follow the new accounting (one strict open for verification and raw opens for the trade and equity scans); no bound was loosened and no row removed. The new pytest rows have no fast label (PR Release runs ctest -L '^fast$' without pytest).

Files: docker/pf_sink.py, docker/run_json.py, docker/run_json_trade_scan_test.py (new), docker/run_json_equity_scan_test.py (new), docker/run_json_bounded_transport_test.py, tests/test_run_json_stream_fastpath.py, tests/test_run_strategy_sink.py, tests/cr150.d/p2cx-rjf1.cmake (new), tests/cr150.d/p2cx-rjf2.cmake (new).

Evidence (test runs on the project's cloud runner)

  • All eight verify steps (preflight, release, debug, sanitizers, kernel, docs, parity, subset) pass on this exact tree with main merged in.
  • The new equity rows against the base without the equity change: 118 of 249 fail (the rows were written before the implementation); the new stream-fastpath rows against the trade-pass change alone: 30 of 280 fail, exactly the five tests that pin the old accounting.
  • Window-edge mutants: a carry of 2 bytes fails 7 trade-scan rows (3 boundary rows, 3 trade-edge rows, the escaped edge at pad 65534); the clean run passes all 159.
  • Frozen-bytes test on a real-sink corpus subset with equity arrays: passes.
  • Whole-population parity sweep of this exact tree (clean engine and codegen main, 134 of 134 cases, 7989 graded probes): the gate passes with no improvement and no regression: coverage lost 0, band 1 +0/-0, band 2 +0/-0, hard regressions 0, leavers 0.

🤖 Generated with Claude Code

luisleo526 and others added 25 commits October 26, 2026 10:16
…al scan of the sink (red first)

Specifies, before the change, how run_json's trade pass reads the verified
report sink: one fresh raw sequential scan whose raw chunks go into a single
sha256 and from which only the T lines (plus the header once) are parsed, via
a public pf_sink.RecordParser built once. Rows pin the parse count (header +
T lines, not every record), the typed refusals (digest differs, file identity
differs, T scan disagrees with its verified count) distinct from the strict
reader's texts, framing across 64 KiB window boundaries, escaped tags,
oversized lines, early close releasing the descriptor, and byte identity of
the trades array against the strict reader and the existing oracle.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
…usal; trailer straddling a window

Adds guards for the new single-scan trade pass: an EIO or a mid-scan
truncation while the scan reads must come out as the typed "report sink is
invalid" refusal with the descriptor released (never a raw OSError, never
"changed after verification"), and a trailer that straddles a 64 KiB window
boundary must stop the hash exactly at the trailer start; an unterminated
trailer there is a format refusal.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
…hashes and parses the same bytes

records("T") no longer re-parses the whole verified report sink with the
strict reader. It opens a fresh raw uncached file, reads it in the existing
fixed 64 KiB windows, feeds every byte before the trailer into one sha256,
and cuts the T lines (plus the header, parsed once) out of that same stream
with bytes.find and a bounded carry; only those lines go through a new public
pf_sink.RecordParser, built once per header and used by SinkReader as well.
The scan compares its digest with the one pass A verified, then its T count
with pass A's count, and refuses with typed texts distinct from the strict
reader's (digest differs / T scan disagrees with its verified count / file
identity differs); an OSError during the scan is the typed "report sink is
invalid" refusal. Pass A, the other record filters and the output are
unchanged. Memory stays flat in bars and trades.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
…edge for the trade scan

The trade pass finds T lines with a bounded carry across fixed windows. Pin it
at every offset from WINDOW-14 to WINDOW+14, for the first T line and for the
trailer start (which then straddles into the next window): each sink holds a
padding row, then literal and escaped-tag E and T rows, and the trade pass
must equal the strict reader's T rows with A's count and no refusal.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
…ss open

records("T") now opens a fresh raw uncached file itself (raw=True) instead of
a strict SinkReader. The test that wraps _open_uncached_sink forwards the
keyword, counts strict and raw opens separately (8 strict: verify, six
filters, the tail E read; 1 raw: the T filter; total equals their sum), keeps
the file-order equality for the raw route, and asserts the raw open is a fresh
unbuffered FileIO at byte zero that is not the strict reader's file.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
The stream-fastpath tests stub run_json._open_uncached_sink with a strict-reader
factory. The trade pass now opens a raw uncached file itself, so the stubs
serve it with real bytes: a raw file over the serialized sink (fresh descriptor,
position zero, bounded reads, counted closes) while the strict route and the
frozen old reference (_OldSink, unchanged) keep the strict reader. Strict and
raw opens are counted and asserted separately, with the same byte-equality
assertions as before; the fabricated receipt digest becomes the real digest of
the serialized prefix so production's digest comparison sees consistent data.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
…de-scan sweep

The sweep placed the literal and trailer needles at every offset around the
window edge but the 4-byte escaped-tag needle only hundreds of bytes away. Add
the escaped edge: the first T line of the sink is the escaped one, at every
offset from WINDOW-14 to WINDOW+14, with its byte position asserted.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
… of the window sweep

The escaped edge swapped two tail rows, which reordered the trades and made
pass A itself refuse the sink (trade rows out of report order). Keep the row
order and escape the first T (which sits at the edge) and one E instead, with
the second T and the other E kept literal.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
PR Release runs ctest -L '^fast$' without pytest (ci_preflight.py), which
failed engine #517's pytest row with that label; #522 removes it there.
Same one-line fix for this row, before the F1 PR exists.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
…ages

Comments, docstrings and three assertion messages only: they name what the
code does instead of the work item it came from. No executable logic changes.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
…equential scan (red first)

Specifies, before the change, how run_json's equity pass reads the verified
report sink: the trade scan generalised to E, one fresh raw sequential file
whose raw chunks go into a single sha256 and from which only E lines are cut and
decoded by a verified-bytes decoder (valid only for bytes whose digest equals
the digest pass A computed in a full strict read). Rows pin the parse count
(one header parse, independent of the E count), the typed refusals (digest
differs, E scan disagrees with its verified count, file identity differs)
distinct from the strict reader's, a differential corpus against the strict
parser (types, -0.0, float spelling, I53 bounds), hostile bytes mapped to typed
refusals, pass A not weakened, window-edge sweeps, provisional-row discard and
early close.

The one F1 row that pinned the old strict error for E is narrowed to None and K;
E moves to the new rows.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
Tests only. R1: the zero-byte-body pipeline half expects the digest text,
not a blank-line refusal that the scan cannot reach. P2: the non-list and
wrong-tag shapes move to direct decoder rows. C3: every accepted float
token comes from repr(), with named non-repr alternates per column.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
…rict walk

Pass C (records("E")) now shares F1's raw sequential scan: the tag, needle,
decoder and count key are parameters of _tagged_records, T keeps its
behaviour. RecordParser.parse_verified decodes an E line after pass A has
verified the sink: same framing texts, same _field and token hooks, no
lexical byte walk. Refusal order is framing/EOF, digest, count; pass A is
unchanged. The wrong-tag row pins the decoder's own text, and a
whitespace nit in the trade-scan sweep is fixed.

The stream-fastpath rows that pin the old strict pass-C accounting follow
in the next commit.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
PR Release runs ctest -L '^fast$' without pytest; engine #522 removed the
label from the #517 pytest row, and the trade-scan row follows the same
rule. Same fix for this row.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
The report writer now opens the sink once strictly (pass A) and twice raw
(pass B for trades, pass C for equity). Re-derive the stream-fastpath
budgets from that design: strict and raw opens are counted per tag by the
single open_sink factory, successful unnamed reports must open one raw
stream per writer, and the equity-writer trailer row expects the digest
refusal text. Every equality stays an equality, the frozen _OldSink and the
report byte assertions are untouched, and the collected rows are unchanged.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
…ity tests by behaviour

records("E") now opens one raw stream, so the report-sink method row expects
three raw opens (trades, equity, the tail equity read) and six strict opens,
and the changed-after-verification row expects the digest refusal text for
the equity filter. The equity-scan file and its cmake fragment describe what
they test instead of the work item they came from.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
…y-scan assertion message

Message text only.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
…writes

The trade pass now refuses a sink whose inode or size differs from the one
recorded at verification. This test deliberately rewrites the sink and
replaces its digest and counts to simulate a newly sealed short equity
sequence; it now replaces the recorded identity the same way, so the trade
read-back it asserts still sees the sealed file.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
@luisleo526
luisleo526 changed the base branch from report-sink-trade-scan to main October 26, 2026 16:42
@luisleo526 luisleo526 changed the title report sink: read equity in one raw sequential pass, decoded once verified report sink: read trades and equity in one raw sequential pass each Oct 26, 2026
@luisleo526
luisleo526 marked this pull request as ready for review October 26, 2026 17:05
@luisleo526
luisleo526 merged commit 2cd229f into main Oct 26, 2026
16 of 17 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant