The Subscribe panel initializes its coupon model only when the layout owner's live data has no value for that input. Status changes keep the entered code. The framework text field uses immediate input with no debounce, and redemption captures the typed input before changing the panel to “Checking the coupon…”. Coupon validation, entitlement writes, install behavior, and content registration still use the existing Store funnel.
The text field's code pointer is relative to its coupon-model data context. An absolute /code
pointer starts at the layout root, bypasses that context, and redraws the field blank even though
the acknowledged coupon model still contains the typed value. The regression resolves the actual
rendered control through the GUI binding API, so it covers the same pointer and context the browser
uses rather than inspecting only the server-side model.
The old panel rebuilt host.Edit(new CouponInput(), id) on every status emission. That editor
bound a fresh blank model to the same data key. A server-acknowledged client edit was therefore
lost during a status redraw. An idempotent check against the render snapshot is insufficient:
the top-level renderer accumulates its own snapshots. Initialization instead goes through
host.Update against the current owner collection, outside the control's render data.
Store/Plugin/Test/CouponInputTests.cs contains both live regressions and PluginTestsArea
registers them in the normal Store/Plugin Tests area. The redraw case renders the actual coupon
panel, then queues an identity read on the same layout owner after each render. Typed,
JsonElement, and JsonObject values must survive idle, redeeming, and validation-error states.
The click case resolves and validates the rendered field's pointer, data context and immediate
write settings, records the acknowledged edit on the layout owner, fences on that serialized
owner, and dispatches the real ClickedEvent to the rendered Redeem control. Its random
non-empty code must reach lookup
and report Invalid coupon code.; Enter a coupon code. proves the redraw discarded the edit.
Because the synthetic code does not exist, this click case cannot grant an entitlement or start
an install. Both cases restore the original input and status slots on the owner queue and wait
for that update before succeeding; they also clean up on failure.
Baseline and candidate proof — 11 September 2026
A focused host fixture used actual LayoutAreaHost, the remote layout client, and
UpdatePointer to enter a synthetic coupon. It waited for the owner's populated echo before
changing status, observed the new error control, and then read the owner inside its serialized
update queue. This avoids mistaking an earlier replayed value for retained input. Two further
fixture cases execute the exact native regression above, verifying restoration of an existing
input and removal of a previously absent slot.
All three cases failed against unchanged Plugins 481298ac3f1bf4fe5a6af843b4fc60adf023faae
product source, and all three passed after this correction. The native case covers nine shape/status
combinations. The isolated framework checkout was core
b65c1957de948673c997c0584f13773c41c3e101, matching the failed Education CI framework source.
The Store/Plugin library was emitted by that core's unchanged canonical compile-check.py
using its declared source, test, and configuration closure; the host fixture built strictly
with zero warnings and errors. It references a library, never a test executable.
The local receipt is /private/tmp/coupon-input-results/final-receipt.json; adjacent
before-final.trx and after-final.trx record 3 failed then 3 passed, with the same test
source and framework references. host-fixture/ preserves the narrow SDK wrapper, and
/private/tmp/coupon-input-node-library/inputs.json records compiler inputs. These are local
investigation artifacts, not deployable dependencies. The durable regressions run through
Store/Plugin's normal Tests area in CI. The click case covers the native bound-write-to-action
path; a real browser and production redemption remain separate delivery checks because this
test deliberately uses a missing code and makes no grant.
The second half: the edit that never left the browser — 13 September 2026
252b87a1 fixed the REDRAW (an absolute /code pointer re-rendered the field blank). #1698 was
reopened the same evening because the assertion still failed on three sealed sets that carry it, and
the remaining cause is one level below this panel: the typed value had not reached the owner when
the click was handled, so RequireFirst(host.GetDataStream<CouponInput>(id), …) read the seeded
blank and RedeemFree answered Enter a coupon code. The seed is what makes an unarrived edit and
an empty field the same state — which is why the panel could answer at all rather than hang.
It is not a transport race. FormEditReachesTheOwnerBeforeTheClickTest.APastedValueIsWhatTheClickSubmits
enters one value and raises the click in the SAME dispatcher turn, and it passed on unmodified
main (plugins c4f67f8c, platform 7ee11bc7): an edit already handed to the stream arrives ahead
of a click raised in the same breath. So the question is whether the edit was handed over at all.
It was not. FormComponentBase throttles its write-back (leading edge, 20 ms) and flushes
whatever the cooldown still holds when the field loses focus — "a user could edit a value and click
away before the debounce timer fires, causing the change to be silently lost", its own comment.
That flush sat behind ViewModel is not { IsBlurable: true }, and FormControlBase sets
IsBlurable = value != null from whether the control was given a server-side OnBlur HANDLER. The
two questions are unrelated: one asks whether a BlurEvent is worth posting to the owner, the other
whether this field's own pending edit may be committed. Folded into one guard, the flush never ran
for any field without a blur handler — the coupon box included — which is exactly the "edit, then
click away" case it was written for. MeshWeaver.Education's install e2e calls await code.blur()
before pressing Redeem precisely to "force the component's change → binding commit"; on this field
that call committed nothing.
The flush is now unconditional and only the BlurEvent post stays gated. Nothing is slowed down and
no bound is widened, which #1698 asks for by name.
Pinned by three cases in src/MeshWeaver.Blazor.Views.Test/FormEditReachesTheOwnerBeforeTheClickTest.cs,
over the production FormComponentBase and BlazorView bases, one real remote synchronization
stream and a real layout area host in the paywall's shape (a render-seeded data slot, a pointer-bound
field, a click action that reads the slot back):
APendingEditIsCommittedOnFocusOutEvenWithoutABlurHandler— the witness. Two edits in immediate succession so the second is held in the cooldown, then focus-out, then the click. Onmainit found"WELCOME", the superseded prefix the leading edge had already sent, where the person entered"WELCOME2026".APastedValueIsWhatTheClickSubmits— the positive control described above. Passed before and after; it is what would fail if a "fix" ever stopped a committed edit from reaching a click.AFieldWithNoBlurHandlerStillPostsNoBlurEvent— the other positive control. Dropping theIsBlurableguard outright would satisfy the witness while sending the owner a blur for every field in the portal; this holds the line at "the flush is local, only the post is gated", and proves the focus-out ran by asserting the commit landed.
What still reopens it. The Education install e2e on an image built from a sealed set carrying
this change. If ⚠️ Enter a coupon code. appears there again, the remaining cause is NOT this one —
the panel's own read is now the only thing left between the box and the redemption, and the surface
to compare against is the harness read in #1698's body.
It did reopen, and the remaining cause was not the panel's read either. See below.
The third and last one: the edit left the browser, and went somewhere else — 15 September 2026
#1698 failed again on 2026-09-15 at 05:02:38Z (Education run 34930063765, set 3.0.0-ci.8630,
plugins f716e87c79), on a set carrying BOTH fixes above — verified by git merge-base --is-ancestor, not assumed. The portal was healthy ([LIVENESS] tick=60 one second earlier, pool
empty, +212,363 completions on that tick) and the SAME portal had redeemed a coupon minutes before,
so it was neither a stall nor a wholesale break.
The mechanism is not this panel at all — it is where a field's write LANDS. BlazorView read its
data context ONLY from the [CascadingParameter], a value DispatchView delivers out of band, while
the control it renders carries the same value as a direct [Parameter]. A view could therefore bind
and write with NO context while holding one. UpdatePointer then passed DataContext ?? "/" to
core's GetPointer, which re-roots a relative pointer onto the layout document itself. So the
typed code went anywhere but /data/store-plugin-coupon/code; this panel's guarded seed was still
the only thing in the slot; RedeemBounded read it; RedeemFree refused an empty code — and the box
still showed WELCOME2026, because FormComponentBase sets its local value before any of this.
Nothing threw and nothing was logged, because the coupon pointer is ONE segment. The quiz's
answers/q1 crashes loudly on the same path ('q' is an invalid start of a value); a bare code
resolves silently against a root key of the layout stream's own document. The loud casualty was found
in a week; the silent one took three rounds.
Why the two fixes above could not have held. 252b87a1 is what put the field on this path: it
changed the pointer from the ABSOLUTE /code to the relative code plus a control-level
DataContext, trading a deterministic redraw defect for a context-dependent one — which is why the
issue was reopened the same evening on sets that carry it. 04f381a0 made a pending edit flush on
focus-out; that guarantees UpdatePointer is CALLED and has nothing to say about where the call
lands. Both are real fixes for real defects. Neither is this one.
Fixed in the platform by #1899 (30cbf26df, "a view binds against its control's own data
context, not only the cascade"), merged 2026-09-15T06:23Z — 81 minutes AFTER the failure above,
on a commit the failing set does not contain. BlazorView.BindingDataContext now prefers the
control's own declaration, and a relative pointer with no context anywhere is refused in both
directions rather than re-rooted. The full account is
Default Views — a view binds against its control's own data context.
Nothing in this panel changed for it, and deliberately so: the field declares its context, which
is the contract. Re-spelling the pointer as an absolute /data/store-plugin-coupon/code to route
around the platform would work and would be a band-aid — it would leave every other control that
declares a context on the broken path.
Pinned by CouponFieldWritesThroughItsOwnContextTest (src/MeshWeaver.Blazor.Views.Test), in
this panel's exact shape: a render-seeded coupon model, a field bound to one of its members through a
relative pointer and its own DataContext, one real remote synchronization stream, a real layout
area host, and a real BlazorView.OnClick whose action reads the model back off the OWNER. Its
TheEditLandsInTheCouponModelAndNowhereElse row asserts the entered code exists at exactly ONE
position in the layout document — the only assertion that separates a re-rooted write from an absent
one. Measured with the platform fix reverted: all four rows fail with "" where the person entered
WELCOME2026, and all three rows of FormEditReachesTheOwnerBeforeTheClickTest PASS — that suite
binds an ABSOLUTE /data/{id} pointer and is structurally unable to see this, which is the same
property that made its APastedValueIsWhatTheClickSubmits pass on main before #1815.