From 531755acb27851887b212f00295270f65638611b Mon Sep 17 00:00:00 2001 From: luisleo526 Date: Fri, 9 Oct 2026 16:27:32 +0800 Subject: [PATCH 1/2] docs: state the report-ownership contract for the paired engine fix MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add "A report belongs to the call that returned it" to the public contract, a pointer in the README's run section and an Unreleased changelog note. 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. Callers read the error text, the error code and the run status, because a refusal before the engine admits the begin leaves the status at its earlier value. A batch or reentrant begin over a live stream latches Failed and a repeated stream begin does not; a configure time calendar failure leaves the run-number high-water where a failure inside the begin has already moved it. The text is framed as the contract of an engine fix that is not yet released: it names no release number and states that engine v1.4.0 does not carry it. Markdown only; no source, fixture, version, lock or workflow file changes. Rebased onto main 6c8eadb (#193), which added the 1.5.0 diagnostic notes. The changelog note now sits under its own "Planned 1.4.0-line patch — Unreleased" heading after main's unchanged "1.5.0 — Unreleased" section, and its opening is scoped to this documentation change: it adds no Python, JSON, diagnostic or emitted-C++ change, keeps the 1.5.0 diagnostics separate and names no tag, date or version. The contract's availability paragraph now says the report fix itself changes neither the generated C++ nor the transpiler's Python and JSON contract, so it does not contradict the "since 1.5.0" text beside it. Co-Authored-By: Claude Sonnet 5.5 --- CHANGELOG.md | 25 +++++++++++++ README.md | 12 ++++++ docs/PUBLIC_CONTRACT.md | 81 ++++++++++++++++++++++++++++++++++++++++- 3 files changed, 117 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index fba3cbc..2fca598 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -91,6 +91,31 @@ diagnostics only; no analyzer or code-generation file differs. this entry, and no diagnostic carries a call-site identifier or a startup length. +## Planned 1.4.0-line patch — Unreleased + +A planned patch release of the 1.4.0 pair. It has no tag, date or version yet, +and these notes are a plan, not a record. This documentation change adds no +Python, JSON, diagnostic or emitted-C++ change of its own. The diagnostic +additions under 1.5.0 — Unreleased are a separate change and are not part of +this planned patch. The engine side is a report fix, in preparation, that engine +`v1.4.0` does not carry. + +### Documentation + +- 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 From 7975847ec7f2fa4aaee84b60d8d1395a0899f70f Mon Sep 17 00:00:00 2001 From: luisleo526 Date: Fri, 9 Oct 2026 19:22:26 +0800 Subject: [PATCH 2/2] docs: move the planned patch notes under 1.5.0 The report-ownership notes were drafted as a planned 1.4.0-line patch. The paired engine's report fix is planned for 1.5.0, so the notes now sit in a Documentation subsection of the 1.5.0 entry, and the entry's opening names that scope. The codegen change stays diagnostics only. Only CHANGELOG.md changes: README.md and docs/PUBLIC_CONTRACT.md are byte-identical to the reviewed commit 531755a. Co-Authored-By: Claude Haiku 5.5 --- CHANGELOG.md | 20 ++++++++------------ 1 file changed, 8 insertions(+), 12 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2fca598..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,17 +93,11 @@ diagnostics only; no analyzer or code-generation file differs. this entry, and no diagnostic carries a call-site identifier or a startup length. -## Planned 1.4.0-line patch — Unreleased - -A planned patch release of the 1.4.0 pair. It has no tag, date or version yet, -and these notes are a plan, not a record. This documentation change adds no -Python, JSON, diagnostic or emitted-C++ change of its own. The diagnostic -additions under 1.5.0 — Unreleased are a separate change and are not part of -this planned patch. The engine side is a report fix, in preparation, that engine -`v1.4.0` does not carry. - ### 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;