Repository navigation
Fail the run on an unaccepted timezone or a failed void setter - #837
Conversation
The scoped-timezone mechanism ignored the result of setenv, and a memory failure while a zone was switched left the caller in the previous zone or in UTC. A zone name written in a script reached setenv and tzset unchecked in three families (the time fields hour to weekofyear, time() and time_close() with the session predicates, and str.format_time), and an unknown name such as "America/Newyork" silently read as UTC, where TradingView stops the run. The fourteen void C setters for the instrument and run settings swallowed every exception, whether an allocation failure or a change the engine refused, so a run could start with a setting that was never applied and finish with a wrong result. An unaccepted timezone name now stops the run with the new code pine_timezone_error (class strategy, not retryable, since 1.12.0). Its arguments are bar_index, function and zone. The message is TradingView's text (runtime error RE10133) with the engine's bar prefix, "Error on bar N: ", or "Error before the first bar: " when no bar is being executed. The prefix comes from the one function that already builds it for the regular-expression stop, run_error_bar_prefix; a second function, run_error_bar_index, supplies the argument (-1 when no bar is being executed). Generated code reaches the stop through pine_timezone_stop(function, zone, english), offered under its own macro, PINEFORGE_HAS_TIMEZONE_STOP_V1; the caller passes the English without a prefix. TradingView's function name is captured for time and timestamp only: the other ten names of the vocabulary are the engine's choice, and the session predicates report time although their zone is the symbol's. Accepted names are remembered in a table of eight slots per thread, so a repeated name is not checked again. A failed setenv (out_of_memory for ENOMEM, engine_unclassified_error with the errno kept for any other error) and a memory failure while the timezone is switched now fail the run. The void C setters now record every exception on the handle: strategy_last_run_status reads 1, and strategy_get_last_error and strategy_get_last_error_code return the text and the code. The text begins with the same bar prefix. A setup failure makes the next run start fail once with that record; the following start clears it and starts normally. The failed setter did not apply its value, so a retry of the start without applying the setter again runs on the previous value. The documented refusal of a quote write after a selected run has sealed is unchanged: it is still dropped silently and read through strategy_state_query_status_v1. Non-positive or NaN mintick and pointvalue in a --syminfo document, and path_order modes outside 0, 1 and 2, now give typed refusals (the existing symbol metadata error for the tick and the point value, setting_rejected for the mode) instead of being ignored or reset to AUTO. The new tests use a counting replacement of the global operator new (tests/fail_alloc_seam.hpp) to refuse each allocation of a run in turn: every allocation must end in a typed failure or in a closed-trade list (and its totals) identical to the clean run, never in a different completed result, and the sweeps print the indexes at which a run completed. The setenv failure is injected by an interposer in the test, with no hook in the production code. One test covers the timezone name check and the zone switch, one the setter paths, and a Python test reads the --syminfo refusals. The new code is also registered where codes are counted and announced: tests/test_run_failure_code_numbers.py pins its number (88) and the enumerator count (89), the page docs/pages/run-failure-codes.md counts 88 named codes and 89 registry entries and names the code in its preamble, and the registry count pinned in tests/test_run_failure_codes.cpp becomes 89. Documentation sentences this change removes or rewrites: - docs/pages/run-failure-codes.md, the preamble: "The catalog contains 87 named failure codes: 38 first released in 1.4.0, 26 first released in 1.5.0, 5 added in 1.7.0, 10 added in 1.9.0 (3 run-quota codes and 7 report-channel codes), and 8 added in 1.11.0." and "One unnamed `none` sentinel gives 88 registry entries; existing numbers stay fixed." They now count 88 named codes (one added in 1.12.0) and 89 registry entries. - docs/pages/live-surface.md: "Any other `mode` is clamped to AUTO." An out-of-range path_order mode now records a typed setting_rejected error. - docs/adr/0001-kernel-adapter-boundary.md, the row of strategy_set_path_order: "clamps an out-of-range mode to AUTO" and "A `void` ABI v4 setter cannot answer a refusal." The setter now refuses and records setting_rejected, readable through strategy_get_last_error, strategy_get_last_error_code and strategy_last_run_status; the void signature is unchanged. - docs/coverage.md, the pine_str_format_time row: "Empty / `"UTC"` / `"Etc/UTC"` use `gmtime_r`; everything else swaps `TZ` under `tz_util::ScopedTimezone` and uses `localtime_r`." It now says "other accepted names" and describes the new error. - docs/pages/run-failure-codes.md: "hardwires its code and takes only literals the transpiler emits"; pine_timezone_stop takes the zone string the script computed. - docs/adr/0001-kernel-adapter-boundary.md, the row of run-failure code names: the sentence "Each name states the failure in the terms of the script that caused it" gains the timezone argument. ADR-0001 lists pine_timezone_error with the other code names and gives pine_timezone_stop its own row, because the kernel calls it. The ownership manifest lists the new code. The catalog gains the code, so a release that carries this change is a minor one. The allocation-bound manifest lists three new groups (the number-to-text temporaries of the setter refusal messages, and the message string of the timezone refusal with its temporary; ceiling 1415 -> 1418, pinned count 1599 -> 1602) and its site inventory is re-emitted, and the allowlist of uncoded rethrows counts the two new rethrows. The lines added to the sources move the lines the documentation cites below them; the anchors that follow them are re-pointed by the number of lines added (digits only). Five pre-existing sentences of docs/adr/0001-kernel-adapter-boundary.md (two) and docs/design/native-feature-parity.md (three) are also reworded, because the re-pointed lines count as added lines and the older wording named an internal work stream by a word that public text does not use; they now say "work item" and mean the same. Co-Authored-By: Claude Fable 5.2 <[email protected]>
There was a problem hiding this comment.
Graphify reviewed this change.
Worth a look — the grounded gate found no coupling regressions or blocking issues, but 2 advisory finding(s) below merit a look before merge.
Not checked: tests were not run; no formal proof of the changed code.
Graphify review — findings
Fails the run with typed errors where the engine used to fall back silently: a failed setenv or a memory failure during a timezone switch no longer continues in the previous zone or UTC, and an unaccepted timezone name raises the new pine_timezone_error (a public failure-catalog addition, so a minor release) instead of reading UTC. The void C setters for instrument and run settings now record exceptions on the handle, so strategy_last_run_status reads 1 and strategy_get_last_error and strategy_get_last_error_code report the text and code. A setup failure makes the next start fail once, the following start clears it, and the failed setter's value is not applied. Non-positive or NaN mintick and pointvalue values in --syminfo, and path_order modes outside 0, 1 and 2, now return typed refusals instead of being ignored or reset to AUTO. The CR150 source-bounds ceiling moves from 1415 to 1418 unreviewed groups for the new failure-path message temporaries.
Worth a look
- Positive infinity passes symbol metadata validation —
docker/run_json.py:3479· Escalate · medium- unverified hypothesis — survived the adversarial refute pass; needs human review
- Pre-first-bar timezone errors render the wrong message prefix —
docker/run_failure_codes.json:883· Escalate · medium- unverified hypothesis — survived the adversarial refute pass; needs human review
Review partial — this diff was larger than one review pass covers, so later files were not reviewed; some findings may be missing.
Analysis details — impact, health, verification
Impact & health
Graphify review
Impact — 11851 functions depend on the 5721 functions this change touches.
Health — this change adds coupling hotspots:
- worse:
tf_change()— 28 callers, 5 callees - worse:
main()— 1 callers, 67 callees - worse:
note_run_failure()— 19 callers, 3 callees - worse:
crosses_boundary()— 7 callers, 8 callees - worse:
str_tostring()— 8 callers, 7 callees - worse:
native_run_spec_digest()— 17 callers, 3 callees - worse:
build_native_day_partition()— 9 callers, 5 callees - worse:
find()— 10 callers, 4 callees - …and 24 more — each is listed as a finding
Verification — 11851 functions in the blast radius were not formally verified this run (proofs are advisory here).
Gate & verification
graphify gate
PASS — objectively clean (no health regressions, tests not run — proofs not run this pass (advisory)). Grounded, not self-assessed.
Advisory (not blocking):
- verification_scope: 11725 function(s) in the blast radius were not formally verified this run
Test selection
Test selection
1423 of 1423 test file(s) selected (100%) via static blast radius.
Escalated to a full run for safety (
noncode-change,uncovered-code) — the selection is not trustworthy on its own (see below). CI should run the whole suite.
benchmarks/tests/test_aggregate.py— full-run-safetybenchmarks/tests/test_check_provenance.py— full-run-safetybenchmarks/tests/test_compare.py— full-run-safetybenchmarks/tests/test_public_names.py— full-run-safetybenchmarks/tests/test_run_all.py— full-run-safetybenchmarks/tests/test_run_strategy_feeds.py— full-run-safetydocker/pineforge_sink_finalize_test.py— full-run-safetydocker/pineforge_sink_replay_rq_test.py— test-dependency, full-run-safetydocker/pineforge_sink_replay_test.py— full-run-safetydocker/pineforge_sink_to_json_test.py— full-run-safetydocker/quota_columns_test.py— full-run-safetydocker/quota_convert_test.py— full-run-safetydocker/quota_finalize_test.py— full-run-safetydocker/replay_rq_engine_test.py— test-dependency, full-run-safetydocker/run_json_artifact_window_test.py— full-run-safetydocker/run_json_bin_test.py— full-run-safetydocker/run_json_bounded_transport_test.py— full-run-safetydocker/run_json_bulk_equity_fuzz_test.py— full-run-safetydocker/run_json_bulk_equity_test.py— full-run-safetydocker/run_json_codes_test.py— full-run-safetydocker/run_json_converter_metadata_test.py— full-run-safetydocker/run_json_converter_session_test.py— full-run-safetydocker/run_json_diagnostics_test.py— full-run-safetydocker/run_json_equity_scan_test.py— full-run-safetydocker/run_json_fused_test.py— full-run-safetydocker/run_json_harnesses_test.py— full-run-safetydocker/run_json_metrics_only_test.py— full-run-safetydocker/run_json_misc2_test.py— full-run-safetydocker/run_json_misc_test.py— full-run-safetydocker/run_json_pfbar_test.py— full-run-safetydocker/run_json_provenance_test.py— full-run-safetydocker/run_json_quota_supervisor_test.py— full-run-safetydocker/run_json_replay_test.py— full-run-safetydocker/run_json_selected_sink_test.py— full-run-safetydocker/run_json_sink_lexical_test.py— full-run-safetydocker/run_json_sink_summary_test.py— full-run-safetydocker/run_json_sink_test.py— full-run-safetydocker/run_json_sink_vectors_test.py— full-run-safetydocker/run_json_spool_work_test.py— full-run-safetydocker/run_json_stringio_test.py— full-run-safetydocker/run_json_symbol_feeds_test.py— full-run-safetydocker/run_json_syminfo_test.py— full-run-safetydocker/run_json_trade_scan_test.py— full-run-safetydocker/run_json_window_budget_test.py— full-run-safetydocker/run_json_wrapper_work_test.py— full-run-safetyinclude/pineforge/native_run_spec.hpp— test-dependency, full-run-safetyscripts/derive_corpus_feeds_self_test.py— full-run-safetyscripts/fingerprint_self_test.py— full-run-safetyscripts/run_corpus_self_test.py— full-run-safetyscripts/test_abi_receipt_skips.py— full-run-safety- … and 1373 more
non-code file(s) changed (
CHANGELOG.md,ci/cr150-source-bounds.yaml,ci/cr150-source-sites.json,docker/run_failure_codes.json,docker/run_failure_codes_diff.json…) → running the full suite for safety (a code graph can't see config/fixture/data deps)
changed code file(s) with no mapped test (
CHANGELOG.md,docs/adr/0001-kernel-adapter-boundary.md,docs/coverage.md,docs/design/BARS-BIN-INPUT.md,docs/design/arena-bound.md…) — a coverage gap or a missing link — running the full suite rather than only the selected tests
Selection is safe under the controlled-regression assumption; always-run tests + a periodic full run are the backstops. Advisory — it never changes the check verdict.
Docs that may be stale (advisory)
docs/ci.md§ Allowed status creators (lines 2012-2024): references changed symbolsrecorddocs/design/arena-bound-corpus-census.md§ Reproducible rule (lines 71-111): references changed symbolsrecorddocs/design/o1-selected-capture.md§ Presentation hooks and compatibility (lines 109-147): references changed symbolsHostdocs/design/report-channel.md§ R05 — All native sink routes are bounded (lines 110-119): references changed symbolsrecorddocs/pages/abi-stability.md§ Symbol inventory (lines 111-240): references changed symbolsHostdocs/pages/abi-stability.md§ What's *not* guaranteed (lines 360-666): references changed symbolsHost,recorddocs/pages/sink-vectors.md§ Report sink golden vectors {#sink_vectors} (lines 1-138): references changed symbolsrecorddocs/pine-adapter-kernel-notes.md§ 2.1 Per-entry-id unclosed-quantity ledger (lines 723-748): references changed symbolsrecorddocs/pine-adapter-kernel-notes.md§ 4. Other orphaned fill / bracket / margin notes (lines 819-1024): references changed symbolsrecordtests/ownership/README.md§ Ownership probes (lines 1-277): references changed symbolsrecord
· 17 grounded finding(s) anchored inline below; 15 more finding(s) on lines outside this diff (see the check run).
| session, tz_util::resolve_script_timezone(tz, "time"), bar_ms); | ||
| } | ||
|
|
||
| bool session_in_premarket(const std::string& session, |
There was a problem hiding this comment.
session_in_premarket()
high coupling complexity (Ca·Ce = 12).
Grounded coupling-delta finding (deterministic), not an LLM guess.
| @@ -1641,8 +1677,9 @@ bool session_in_premarket(const std::string& session, | |||
| } | |||
|
|
|||
| bool session_in_postmarket(const std::string& session, | |||
There was a problem hiding this comment.
session_in_postmarket()
high coupling complexity (Ca·Ce = 12).
Grounded coupling-delta finding (deterministic), not an LLM guess.
| } | ||
|
|
||
| template <class Call> | ||
| void invalid_zone_row(const char* function, Call call) { |
There was a problem hiding this comment.
invalid_zone_row()
high coupling complexity (Ca·Ce = 16).
Grounded coupling-delta finding (deterministic), not an LLM guess.
| } | ||
| }; | ||
|
|
||
| void native_module_invalid_name_row() { |
There was a problem hiding this comment.
native_module_invalid_name_row()
fans out to 7 callees (efferent coupling).
Grounded coupling-delta finding (deterministic), not an LLM guess.
| } | ||
| #endif | ||
|
|
||
| int main() { |
There was a problem hiding this comment.
main()
fans out to 12 callees (efferent coupling).
Grounded coupling-delta finding (deterministic), not an LLM guess.
| CHECK(strategy_get_last_error_code(nullptr) == nullptr); | ||
| } | ||
|
|
||
| void repeated_path_refusal_sequence() { |
There was a problem hiding this comment.
repeated_path_refusal_sequence()
fans out to 8 callees (efferent coupling).
Grounded coupling-delta finding (deterministic), not an LLM guess.
| CHECK_SEQUENCE(row, host, repaired.value.input_bars_processed == 4); | ||
| } | ||
|
|
||
| void replacement_numeric_refusal_sequence() { |
There was a problem hiding this comment.
replacement_numeric_refusal_sequence()
fans out to 9 callees (efferent coupling).
Grounded coupling-delta finding (deterministic), not an LLM guess.
| CHECK_SEQUENCE(row, host, host.native_state().kind == NativeLifecycleKind::Ready); | ||
| } | ||
|
|
||
| void first_cause_sequence_controls() { |
There was a problem hiding this comment.
first_cause_sequence_controls()
fans out to 8 callees (efferent coupling).
Grounded coupling-delta finding (deterministic), not an LLM guess.
| } | ||
| } | ||
|
|
||
| void void_setter_prefix_controls() { |
There was a problem hiding this comment.
void_setter_prefix_controls()
fans out to 6 callees (efferent coupling).
Grounded coupling-delta finding (deterministic), not an LLM guess.
| } | ||
| } // namespace | ||
|
|
||
| int main() { |
There was a problem hiding this comment.
main()
fans out to 9 callees (efferent coupling).
Grounded coupling-delta finding (deterministic), not an LLM guess.
What was wrong
A timezone name that a script passes to a time, date or format function and that the calendar does not accept was read as UTC, where TradingView stops the run. A failed
setenv, or a memory failure while the process timezone was switched, left the caller in the previous zone or in UTC. The fourteen void C setters for instrument and run settings swallowed every exception, whether an allocation failure or a change the engine refused, so a run could start with a setting that was never applied and finish with a wrong result.What changes
pine_timezone_error(classstrategy, not retryable, since 1.12.0). Its arguments arebar_index,functionandzone. The message is TradingView's text (runtime error RE10133) with the engine's bar prefix,Error on bar N:, orError before the first bar:when no bar is being executed. The prefix comes from the function that already builds it for the regular-expression stop,run_error_bar_prefix; a second function,run_error_bar_index, supplies the argument (-1 when no bar is being executed).setenv(out_of_memoryfor ENOMEM,engine_unclassified_errorwith the errno kept for any other error) and a memory failure while the timezone is switched now fail the run. Accepted names are remembered in a table of eight slots per thread, so a repeated name is not checked again.strategy_last_run_statusreads 1, andstrategy_get_last_errorandstrategy_get_last_error_codereturn the text and the code. The text begins with the same bar prefix. A setup failure makes the next run start fail once with that record; the following start clears it and starts normally. The failed setter did not apply its value, so a retry of the start without applying the setter again runs on the previous value. The documented refusal of a quote write after a selected run has sealed is unchanged.mintickandpointvaluein a--syminfodocument, andpath_ordermodes outside 0, 1 and 2, now give typed refusals instead of being ignored or reset to AUTO.Scope in plain words
TradingView's function name is captured for
timeandtimestamponly. The other ten names of the vocabulary (time_close,hour,minute,second,dayofmonth,dayofweek,month,year,weekofyear,format_time) are the engine's choice, and the session predicates reporttimealthough their zone is the symbol's. Generated code reaches the stop throughpine_timezone_stop(function, zone, english), offered under its own macroPINEFORGE_HAS_TIMEZONE_STOP_V1; the caller passes the English without a prefix. Generated code that does not call it keeps its earlier behaviour until the code generator is changed, which is a separate change in another repository.Known limits, handled in follow-ups
Tests
The new tests use a counting replacement of the global
operator newto refuse each allocation of a run in turn. Every allocation must end in a typed failure or in a closed-trade list (and its totals) identical to the clean run, never in a different completed result; the sweeps print the allocation indexes at which a run completed. On the base tree both new tests fail (42 and 372 failing checks). A Python test reads the--syminforefusals.Release
Minor (1.12.0): the failure catalog gains the code
pine_timezone_errorand its message template, so its diff against the last release reads one added entry. ADR-0001 lists the code and givespine_timezone_stopits own row; the ownership manifest lists the code.CHANGELOG.mdhas the bullets.Squash commit message
Use the commit's own message for the squash commit, so that the list of documentation sentences this change removes or rewrites stays in the history:
🤖 Generated with Claude Code