Skip to content

Recover original child reservations after definition changes - #1145

Merged
rmcdaniel merged 2 commits into
mainfrom
general/1069-retained-child-bindings-20261114
Nov 14, 2026
Merged

rmcdaniel merged 2 commits into
mainfrom
general/1069-retained-child-bindings-20261114

Conversation

@rmcdaniel

@rmcdaniel rmcdaniel commented Nov 14, 2026 •

Copy link
Copy Markdown
Member

A deployment can change the child definition before a lost reservation acknowledgement is recovered. reserve() correctly refuses replacing that pinned definition, but recovery still needs the original descriptor to close the reservation.

Add ChildReservationStore::load() at the recorded host boundary. It authorizes the original identity before storage, checks native ownership before decoding, and returns the exact stored descriptor without resolving or autoloading a workflow class. Changed binding content and missing records refuse. Callers use close() afterward for the current authoritative admission or committed closure.

Focused and adjacent tests: 235 tests / 5,651 assertions PASS, including historical definition recovery, unavailable-class loading without autoload, policy reauthorization, owner substitution and missing-record refusal. ECS and changed-file PHPStan pass. Six historical-load controls run in the unit persistence suite. Hosted qualification and final-head independent review remain pending.

This prepares the retained compensation adapter for #1069. The child participant, atomic child-scope admission, protected business cancellation/commit and complete integration example remain unfinished. #1069 stays open.

@rmcdaniel

Copy link
Copy Markdown
Member Author

Security exact source/API review: PASS for frozen head 98fdc5798fbef0f8a73553407fc24c149fbb0e36, tree f68e0d371fb1e621f933da94deb6d2509fccb70e, direct base afdab2f4b0c4e01fbd1411751480b73e046b6686.

I reviewed the complete three-file delta. Production changes are confined to the additive 21-line ChildReservationStore::load() operation; the reservation, native admission, start, record, close, ordinary-start fence, persistence writer, and wire/value classes are unchanged.

load() first runs the existing authorization boundary with the exact caller-supplied original BindingIdentity, principal, and distinct load operation. The existing helper refuses caller-owned transactions, requires authorization to leave the native transaction boundary closed, and validates the returned policy version before the first protected query. The returned current policy version is deliberately not substituted into the immutable stored reservation.

Storage lookup is only by the deterministic original-generation child ID. The loaded row must match the authorized namespace and native workflow type before the bounded reservation JSON is decoded. Decoding is the existing maximum-32,768-byte/depth-32 closed state path. ChildReservation::fromArray() and PortableDescriptor::encode() treat the historical workflow class as bounded string data; neither resolves, reflects, instantiates, nor autoloads it. After decoding, the complete stored identity must equal the authorized identity, and matching() rechecks row ID, namespace, workflow-class string, workflow type, and the entire immutable reservation descriptor.

The operation performs no write, start, admission, closure, current-run substitution, or absence certification. It returns the unchanged stored descriptor only. A later close() independently reauthorizes, locks, rereads, and validates the current authoritative row before returning an admission or persisting/replaying a closure, so a concurrent state change cannot be promoted from this unlocked recovery read.

The focused additions exercise historical-definition drift, unavailable-class loading with an autoload sentinel, same-key descriptor substitution, missing storage without creation, distinct load authorization with pinned-policy preservation, owner/type refusal before decoding, authorization-before-query, and caller-transaction refusal. Documentation accurately limits this to descriptor recovery and retains the admission/absence boundaries.

This PASS is source-only. Current hosted checks remain required. It does not complete or promote the retained child participant, atomic scope bootstrap, protected business cancellation/commit, full #1069 integration, missing-child settlement, merge, release, or #1130 authority.

@sonarqubecloud

Copy link
Copy Markdown

@rmcdaniel

Copy link
Copy Markdown
Member Author

Security narrow exact-head rebind: PASS for test-only successor f158a29ae4e7f0174f95dd990df81059bbabdefd, tree 02089f73154f01368f3e03e9c3e728a84efe1372, direct parent 98fdc5798fbef0f8a73553407fc24c149fbb0e36.

The reviewed production ChildReservationStore::load() blob and documentation are byte-identical to the prior source/API PASS. No runtime, storage, wire, authorization, reservation, admission, start, or close behavior changed.

The delta only removes the five historical-load cases from tests/Feature/V2/V2ChildReservationTest.php and adds tests/Unit/V2/StateMachine/ChildReservationRecoveryTest.php. I compared all five complete test methods; each is byte-for-byte identical after relocation. The new unit class uses the same SQLite-backed model/store setup and sits under the repository unit suite declared in phpunit.xml, so these controls now exercise the new API in the unit/coverage lane.

One additional unit case changes the selected row namespace to a foreign owner and replaces the protected reservation with invalid JSON. It requires the exact owner-mismatch LogicException, proving the namespace/type gate refuses before protected JSON decoding rather than merely failing during parsing. The feature class still retains load coverage in the backend authorization-before-query, caller-transaction refusal, and reserve/load/close foreign-owner checks.

git diff --check is clean. This rebind preserves the prior bounded source findings and adds no broader scope. Current-head hosted checks, unit coverage, backend qualification, and full quality remain mandatory; no merge, complete #1069, retained-participant, missing-child settlement, release, or #1130 claim follows.

@rmcdaniel
rmcdaniel marked this pull request as ready for review November 14, 2026 10:12
@rmcdaniel
rmcdaniel merged commit 8bb05a6 into main Nov 14, 2026
30 checks passed
@rmcdaniel
rmcdaniel deleted the general/1069-retained-child-bindings-20261114 branch November 14, 2026 11:03
@rmcdaniel

Copy link
Copy Markdown
Member Author

Delivered at normal merge 8bb05a6, tree 02089f73154f01368f3e03e9c3e728a84efe1372 exactly equal to reviewed/tested f158a29. All four actual-main workflows are terminal SUCCESS, including full build 43584177278. The topic was deleted with an exact f158a29 lease; independent remote readback is 404 and the local topic is absent. All local qualification objects were removed. This delivers historical reservation lookup only; retained participant integration and the remaining #1069 business contract stay open.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant