Skip to content

run_json: a report never carries a bare NaN or Infinity token (EN-133) - #384

Merged
luisleo526 merged 4 commits into
mainfrom
lv/en133-pr
Oct 13, 2026
Merged

luisleo526 merged 4 commits into
mainfrom
lv/en133-pr

Conversation

@luisleo526

Copy link
Copy Markdown
Collaborator

run_json: a report never carries a bare NaN or Infinity token (EN-133)

docker/run_json.py serialized trade and summary doubles with the standard encoder's default, so a non-finite number (for example a NaN max_runup from a host's lot-excursion answer) could reach the output as a bare NaN / Infinity token, which is not JSON.

Behaviour

  • The eight trade doubles per row (entry_price, exit_price, qty, pnl, pnl_pct, max_runup, max_drawdown, commission) and the five summary doubles are checked after the report is built, in the ordinary and the Selected-window run. A non-finite value fails the run with report_result_invalid{field: "trades.<leaf>" | "summary.<leaf>", reason: "nonfinite"}; field is an engine-owned literal, never script text.
  • Every report writer is strict (allow_nan=False), so nothing non-JSON can be written. With an acknowledged execution boundary a writer fault stays report_post_execution_failed{phase: serialization}; without a boundary (no --run-phase-fd) the encoder's non-finite refusal is report_result_invalid{field: "report.document", reason: "nonfinite"}; other ValueErrors and the capabilities and --validate-window-only documents keep harness_internal_error.
  • Metrics, the equity curve and recorded outputs keep their existing handling: a non-finite metric stays null.
  • One sentence added to docs/pages/report-schema.md. The three codes are already in docker/run_failure_codes.json; the catalog is unchanged.

Proof (spot box b1, 2026-10-13, tree identical for these four files to the proven head 3b6b56c1; git diff between this branch and that head over the four files is empty)

  • RED on the base product code: the seven new test families fail by name (test_trade_leaf_nonfinite, test_summary_leaf_nonfinite, test_writer_backstop, the unphased-writer and ordinary-typed-leaf rows, test_native_net_profit_nonfinite, test_finite_pnls_overflow_summary_drawdown); the four controls (metric and equity stay null, finite report bytes identical to base, engine non-finite args dropped) pass on base.
  • GREEN on head: pytest tests/test_run_json_nonfinite_report.py tests/test_run_json_selected_window.py 242 passed, 2 skipped (the two native host-library rows, which need a private fixture).
  • Mutants: 9 of 9 fail their named test (trade check dropped, summary check dropped, each of the four writers allowing NaN again, a metric NaN turned into a failure, the unphased conversion removed, every writer ValueError converted).
  • Not covered here: the native host-library excursion rows (private fixture, skipped by name); the corpus sweep.

Part of EN-132 (value ownership). This slice is independent of the others and lands early.

🤖 Generated with Claude Code

luisleo526 and others added 2 commits October 13, 2026 13:48
… and summary values fail as report_result_invalid (EN-133)

json.dump ran with allow_nan=True and the trades list and summary carried raw
float() values, so a non-finite number became a bare NaN/Infinity token that a
strict JSON parser rejects.

- _validate_report_result: after the report dictionary is built (selected and
  ordinary run), the eight doubles of every trade and the five summary doubles
  must be finite, else RunFailure report_result_invalid with an engine-literal
  field (trades.<leaf> / summary.<leaf>) and reason nonfinite.
- allow_nan=False at the report writers and the printed capabilities and
  validation-only documents. A value that still reaches a writer fails while the
  document is in memory (report_post_execution_failed, serialization, behind the
  acknowledged phase boundary). The failure-line writer keeps its default: its
  args are not universally sanitized.
- Metrics, equity curve and recorded outputs keep _num (non-finite -> null).
- tests/test_run_json_nonfinite_report.py: all 13 leaves on both paths, the
  writer backstop, finite-output byte identity against the frozen base producer,
  null controls; the native-host process rows skip unless a host library is
  supplied. One sentence in report-schema.md.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
…esult_invalid (EN-133)

Without --run-phase-fd there is no execution boundary, so the strict
json.dump(allow_nan=False) refusal on the ordinary report used to surface as
harness_internal_error. It now fails with report_result_invalid, field
"report.document", reason "nonfinite", for the encoder's exact refusal
messages only. With an acknowledged boundary it stays
report_post_execution_failed{phase: serialization}; other ValueErrors, and the
capabilities and --validate-window-only documents, keep harness_internal_error.
The old unphased pin is renamed and corrected, with a circular-document control.
One sentence in report-schema.md.

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

import contextlib
import ctypes
luisleo526 and others added 2 commits October 13, 2026 14:14
…ines (EN-133)

tests/private_fixture_reads.evidence pins the line of the pooc close-fill-view
registration in tests/CMakeLists.txt (3251). The EN-133 test registration adds
three lines above it, so the check_private_fixture_reads guard and its control
row (test_private_fixture_reads, test_private_fixture_reads_controls) fail on
the moved line. The pinned line and the line numbers quoted in its reason
text move by three; nothing else changes. Found on the first spot-box run of
the sibling EN-134 slice, which moves the same lines.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
…m a lane commit (EN-133)

test_finite_report_bytes_match_base compares the finite report bytes of the
changed docker/run_json.py with the frozen producer it replaces. The pin named
a commit of the lane's own local base (26296bb) that does not exist in the
engine's history, so on every verify host the fixture's `git show` failed
(exit 128) and the test errored at setup: test_run_json_nonfinite_report
failed in the release, debug, sanitizers and kernel steps of the first
lab-remote run of this PR. The pin is now the engine main commit this change
sits on (7da487a, docker/run_json.py there is byte-identical to the file at
the commit the change was authored on, 9da4c98; both hash to the same
sha256). Nothing else changes.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
@luisleo526
luisleo526 marked this pull request as ready for review October 13, 2026 09:40
@luisleo526
luisleo526 merged commit 1b08c61 into main Oct 13, 2026
32 of 35 checks passed
luisleo526 added a commit that referenced this pull request Oct 14, 2026
* docs: the 1.6.0-rc.1 changelog entry

The changelog section for the first candidate of 1.6.0, tag v1.6.0-rc.1 on
commit 04607c1: the selected-window contract
revision 1.3 and the typed failures behind it (#388), finite trade and summary
doubles in every report writer (#384, #388), the favicon of the API
documentation (#355), the removed CI time limits and command-line options (#378),
and the tests, CI and benchmarks changes since 1.5.0 (#375, #376, #377, #382,
#383, #385, #386, #387).

The entry is inserted above the 1.5.0 section; no existing line changes. The
date is the UTC date of the tag.

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

* README: link the apex pineforge.dev, not the www host

The README header link pointed at https://www.pineforge.dev. The product site
lives at the apex, https://pineforge.dev; the www host is being redirected to
it, and search indexes both forms today. One link changed, nothing else.

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

* CHANGELOG: add the selected-window mode and typed failures to the 1.5.0 notes

Two things that are in the v1.5.0 tag were missing from its notes: the opt-in
selected-window mode with retained pre-roll bars (#363, already in
v1.5.0-rc.1) and the typed failures that docker/run_json.py raises for
exceptions from the selected-window helper modules (_typed_failure).

The new subsection "Additions to the 1.5.0 notes" only states what the code
and the #363 text say, with the same "development-only parity candidate"
limits (contract revision 1.3-parity, no claim of complete value-ownership
coverage, cannot be promoted to stable). It adds lines and changes none.

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

---------

Co-authored-by: Claude Sonnet 5.5 <[email protected]>
luisleo526 added a commit that referenced this pull request Oct 20, 2026
#456)

* report: fail with report_result_invalid when a report metric overflows; annualise cagr proportionally under one day, with a named form flag (CR-202, owner W-73)

Squash of the CR-202 work on engine main 0212cab. Its single parent is engine main, as the GitHub squash writes it.

What changes
- A report metric whose computed value overflows (buy_hold_return, every other report metric) fails the run with the typed failure report_result_invalid, reason nonfinite, instead of writing null.
- Proportional annualisation (owner ruling W-73; docs/design/selected-window-wire-v1.3-revision-3.1.md, docs/pages/report-schema.md). span_years = (last bar time - first bar time) / (365.25 * 86400 * 1000 ms), as before. Under one day (span_ms < 86 400 000) cagr = (E/E0 - 1) * (1/span_years) * 100, the simple proportional form, always, and calmar = cagr / max_equity_drawdown_pct follows. From one day on cagr is the compounded form, unchanged; only if that value is not finite the simple form is used instead (a rare ranking inversion across the one-day boundary, documented). sharpe_bar and sortino_bar use the same formulas on every span and are never absent by span; sharpe_tv and sortino_tv are unchanged. No null for these four on a valid run; every other non-finite value is the typed report_result_invalid.
- The public report names the form under diagnostics.annualization (outside the 135 digest leaves): cagr = "simple_proportional_subday" or "simple_proportional_overflow", and calmar carries the same string when it is not null. The writer derives it from the curve it holds; for a run of one day or more whose compounded form is not finite and whose first mark is not proven to be the initial capital the cagr is still the simple value and only the overflow flag is absent (documented; a runtime export of the capital is carded for a later release). The frozen report structures gain no member.
- C host (src/native_c_host.cpp): a report failure after a completed run is recorded and returns E_STATE, a report failure after an earlier latched failure returns E_RUN_FAILED with that failure; in both the caller's report struct is defined (zeroed local report freed on a throw) instead of half written.
- The native runner keeps its own report format: undefined metrics stay the string "NaN" (runner/README.md); only an infinite value is the typed failure there.
- Disclosed behaviour: an infinite first-open or last-close price and a non-finite selected anchor are now report_result_invalid (feed admission makes the first unreachable).
- kNeutralReportDigest in tests/test_native_anchored_legs.cpp is a lab harvest of the neutral 15 one-minute bar run (its cagr is the simple proportional value on a 14-minute span); the value is taken from the lab log under the fixture authority of CORE-TOP.

Documentation sentences reworded or replaced by this change, each named for scripts/check_doc_reverts.py (the sentence's introducing commit, then its first words):
- 1bd075c (Trading metrics + equity curve in backtest report, #31): "See the per-field doxygen in `<pineforge/pineforge.h>` for the exact rule on every field." in docs/pages/report-schema.md, now "in `<pineforge/pineforge.h>` for the existing field domains; the rule below is authoritative" for cagr on a span under one day and when its compounded form overflows.
- 1b08c61 (EN-133, #384): "net_pnl, avg_trade, best_trade, worst_trade and max_drawdown, must be finite or the run fails with report_result_invalid, field naming the corresponding trades.<leaf> or summary.<leaf> and reason equal to nonfinite." in docs/pages/report-schema.md, extended with win_rate_pct and the equity-curve equity and open_profit leaves.
- 5691220 (ADR-0001 rewritten against HEAD): "compute_equity_stats answers its all-NaN value with no error" in docs/adr/0001-kernel-adapter-boundary.md, now "answers its defined empty-curve metrics with no error".
- 375795a (shard the ownership manifest-controls test, CR-196): "(9, 9, 9 and 8 for shards 1 to 4 of 4: 8, 8, 8 and 7 cases plus the guard)" and "The separate cheap test_shards_partition_every_case_exactly_once guard runs in every shard" in tests/ownership/README.md, now "(10, 10, 10 and 10 for shards 1 to 4 of 4: 9 cases plus the guard in each)" and "Each shard also runs the cheap test_shards_partition_every_case_exactly_once guard".
- file:line citation digits only, wording kept: ADR-0001 (the "inclusive bar metrics" bullet, the "Reporting." bullet, the strategy_configure_native_v1 row, the magnifier_sub_bars_total row), native-feature-parity rows RP2, RP4, S1b and "A zoned chart-day memo", native-refactor-progress (the quiet-bar cost check and the every_gate_is_taken_both_ways citation), native-engine ("MUST mirror" citation of compute_equity_stats).

Documentation sentences the history of this branch introduced and later reworded or removed (none of them is deleted by this squash against main):
- c9ba507 introduced "Report arithmetic that produces a nonfinite value, including overflow, fails with `report_result_invalid` and reason `nonfinite`." and "The engine checks computed values before they can become a metric absence." in docs/pages/report-schema.md, both kept.
- eba8561 (revision 3.1) introduced "The constant policy list is in `docker/report_annualization.def`, shared by the native metric walk and public report writer." in docs/design/selected-window-wire-v1.3-revision-3.1.md; e963e5e (the earlier squash of this work) introduced the revision 3.1 note's null-rule sentences ("The short-span absence applies to cagr, calmar, sharpe_bar and sortino_bar", "Calmar carries the short-span reason with CAGR even when the curve also has zero drawdown", "span_too_short_to_annualize") and the report-schema.md ones; the owner ruling W-73 withdrew that null rule and they are reworded or replaced in this change.

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

* tests: keep the base assertion CHECK(!std::isnan(e.cagr)) in test_metrics_l4d (twin parity)

scripts/check_twin_parity.py requires every assertion literal of the base test_metrics to survive in its switched-route twin test_metrics_l4d.cpp. The proportional-annualisation rewrite of test_equity_stats_drawdown_walk dropped `CHECK(!std::isnan(e.cagr));`, so the source-guard-twin-parity stage failed and the verifier-tests stage of preflight read `not-started` for every later stage. The literal is back, unchanged and still true (cagr is defined on every valid run); the reference comparison below it stays.

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

* tests: re-pin kNeutralReportDigest to the lab harvest of the proportional-annualisation report

The neutral run (15 one-minute bars, a 14-minute span, KernelRecorded) now reports a finite simple proportional cagr (10000 to about 10005, no drawdown) and main's formulas for sharpe_bar and sortino_bar, so nothing is absent and its report digest moves again from the value pinned for the earlier null rule. Value harvested from lab job rj-20261020t035042-1a9c33 (label cr202-v6c-pre-rel, release step, test_native_anchored_legs:262), log line 2525: "neutral report digest 6216030702059937357, pinned 8139101939074791901". Fixture authority for exactly that printed value: CORE-TOP (2026-10-20 12:29). That test was the only failure of that release run (961 of 962 tests passed).

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

---------

Co-authored-by: Claude Sonnet 5.5 <[email protected]>
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