diff --git a/CHANGELOG.md b/CHANGELOG.md index fba3cbc..5ed56f5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -24,9 +24,11 @@ supported as exact pairs; on the 0.x line they are independent. See the The planned number of the next release; it stays **Unreleased** until its tag exists, and the package version is unchanged. This entry records the diagnostic -additions below and nothing else: the engine pairing, the rest of the content -and the date of 1.5.0 are set when the release is prepared. It changes -diagnostics only; no analyzer or code-generation file differs. +additions below and the report-contract documentation for the paired engine +fix, and nothing else: the engine pairing, the rest of the content and the date +of 1.5.0 are set when the release is prepared. The codegen change is +diagnostics only; the report documentation adds no Python, JSON, diagnostic or +emitted-C++ change of its own, and no analyzer or code-generation file differs. ### Compatibility and migration @@ -91,6 +93,25 @@ diagnostics only; no analyzer or code-generation file differs. this entry, and no diagnostic carries a call-site identifier or a startup length. +### Documentation + +The engine side of the report-ownership text below is a report fix, in +preparation, that engine `v1.4.0` does not carry. + +- The [public contract](docs/PUBLIC_CONTRACT.md#a-report-belongs-to-the-call-that-returned-it) + states that a report belongs to the call that returned it. A call the engine + refuses without beginning gets the empty report, never an earlier run's rows; + a run that began and then failed keeps its partial rows; the explicit stream + snapshot keeps the owner's rows; recorded outputs keep their own record of + the last run that began. +- Read the error text, the error code **and** the run status after every run. + A refusal the engine makes before it admits the begin leaves the status at its + earlier value (0 after a completed run), so the status alone misses it. The + README's "Compile & run against the engine" section says the same. +- Pairing is unchanged in kind: regenerate the strategy C++ and relink against + the engine release's headers and `libpineforge.a` on a pair change (README + [Engine pairing](README.md#engine-pairing)). + ## 1.4.0 — 2026-10-08 A minor release of the exact engine/codegen pair. Its behavior was validated diff --git a/README.md b/README.md index 657c51a..d32b4c0 100644 --- a/README.md +++ b/README.md @@ -609,6 +609,18 @@ presence of `strategy.close` or `strategy.close_all` in the source. Older C++ has removed; regenerate it before compiling there. Reachable close commands keep their ordinary runtime lowering. +Check how a run ended before you use its report. Read the error text +(`strategy_get_last_error`), the error code (`strategy_get_last_error_code`) +**and** the run status (`strategy_last_run_status`): the run failed when the +text or the code is non-empty or the status is 1. A refusal the engine makes +before it admits the begin, such as a bar array with a non-finite price, leaves +the status at its earlier value, so a rerun on a handle whose last run completed +can read status 0 beside a non-empty error. An engine without the report gate +(`v1.4.0` and earlier) can also hand a refused rerun on a reused handle the +previous run's rows. The +[public contract](https://github.com/pineforge-4pass/pineforge-codegen-oss/blob/main/docs/PUBLIC_CONTRACT.md#a-report-belongs-to-the-call-that-returned-it) +states the report rule and the engine release that carries it. + Prefer no local build? [pineforge.dev](https://www.pineforge.dev) runs a free hosted MCP server, with a weekly backtest quota, whose `backtest_pine` tool transpiles and backtests a strategy for an AI agent. The diff --git a/docs/PUBLIC_CONTRACT.md b/docs/PUBLIC_CONTRACT.md index 8347617..eb8be36 100644 --- a/docs/PUBLIC_CONTRACT.md +++ b/docs/PUBLIC_CONTRACT.md @@ -3,7 +3,9 @@ The supported Python/JSON contract began in 1.0.0; the sections below identify later additions. This revision includes the 1.4.0 pair (2026-10-08). Pre-tag validation used codegen `bfc4ddce` and engine `b3192bfc`, whose -version files at that point read `1.3.0`. +version files at that point read `1.3.0`. One section, +[A report belongs to the call that returned it](#a-report-belongs-to-the-call-that-returned-it), +describes a paired engine fix that is not yet released. Text marked **since 1.5.0** describes diagnostic additions that 1.4.0 and earlier releases do not have: the `note` severity, the `user_message` template @@ -172,6 +174,83 @@ the paired engine now reports `setting_rejected` with class `strategy`. The installed engine entrypoint returns exit 4 for all three codes: exit 4 alone does not select a billing or refund decision. +## A report belongs to the call that returned it + +Availability: **not yet released.** This is the contract of a report fix in the +paired engine that is in preparation. Engine `v1.4.0` and earlier do not carry +it; this section applies to the first engine release that does. The report fix +itself changes neither the generated C++ nor the transpiler's Python and JSON +contract: the wrappers still run the strategy and then ask the engine for its +report. + + +A generated `run_backtest` and `run_backtest_full` run the strategy and then +ask the paired engine for its report. The engine hands out the rows of the +attempt that call began. For a call that did not begin (a bar array or run +option it rejects, a handle that is not Ready, a calendar or timezone refusal, +a begin over a live stream) it hands out the empty report instead: every array +`NULL`, every count 0, overwritten and never freed. The report never answers a +refused call with the rows of an earlier run. A refused call on a fresh handle +already read empty; the rule extends that to a handle that has run before. + +A refusal records its cause on the failure channel, `strategy_get_last_error` +(text) and `strategy_get_last_error_code` (code), and most refusals also set +`strategy_last_run_status` to 1. Not all do. A call the engine refuses before it +admits the begin writes the text and the code and leaves the run status as it +was: a bar array whose open, high, low or close is not finite (the source host +that a generated strategy derives from validates the array first) and a +repeated stream begin on a stream that is already realtime are two. A rerun on +a handle whose last run completed then reads status 0 together with a non-empty +error, the same status that the settings extension below calls "completed" for +an ordinary batch preparation failure. Read the error text, the error code +**and** the run status after every run. The run failed when the text is +non-empty, the code is non-empty, or the status is 1. The status alone misses +the refusals that leave it at 0, and the report alone does not tell a failed run +from a completed one. The paired engine's run harness applies this rule. + +What a refusal does to the handle depends on why the call was refused, and the +report fix changes none of it. A begin on a Completed, Failed or unconfigured +handle, and a rejected bar array or run option, leave the lifecycle, the run +identity and the retained rows as they were. A calendar or timezone failure +inside the begin itself comes after the consumed run-number high-water moved +and fails the lifecycle; a calendar or timezone parse failure at configure time +comes earlier, fails the lifecycle there, and leaves the high-water where it +was. A batch begin over a live stream, or a begin made from a callback, is a +contract failure that latches Failed, so the stream cannot be driven on +afterwards, although its snapshot stays readable. A repeated stream begin on a +stream that is already realtime is the exception: the engine refuses it before +it admits the begin, latches nothing, and the stream stays running. + +A run that began owns its rows whatever became of it: a completed report and the +partial rows of a run that began and then failed are handed out as they are, and +the failure is on the same channel. The engine's explicit stream snapshot, +`strategy_stream_fill_report`, asks for the rows' own owner. A live stream's +rows survive a batch call refused over it, which itself reads empty, and the +batch reader stays empty after the snapshot until the next run call. When no +live attempt owns rows (a refusal with nothing running) the snapshot is the +empty report. A run call made from inside a running run's callback reads empty +in the same way, while the outer run, which began, hands out its own rows when +it ends. The snapshot returns 0 when it wrote its output and -1 for a NULL +handle or output and for an exception contained at the C boundary. In that +case the output may hold the arrays built before the exception, so pass a +zero-initialized `pf_report_t` and release it with the library's `report_free` +(the engine's `strategy_native_report_free_v1` for a host that links the kernel +archive on its own) whatever the result. + +Recorded outputs (`strategy_outputs_*`), on an engine that provides them, keep +their own record, the last run that began, and their own documentation. The +rule above does not cover them and this section does not change them. Engine +`v1.4.0` does not provide them. + +This needs an engine release that carries the report gate: the first release +that contains this change. Against an older engine, a rerun on a reused handle +can return the previous run's rows together with the refusal, so read the +failure channel and the status after every run. Regenerate the strategy C++ and +relink against the new engine as for any pair change. + ## Optional generated settings extension From 1.1.0 on, C++ compiled against an engine providing