Refuse a reseed once the reader has supplied their own value #211
Loading…
Reference in a new issue
No description provided.
Delete branch "reseed-preserves-typed-coordinates"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Closes task 210. Defensive: neither hazard is reachable through the application today, and this lands before the change that would make them live rather than after.
The hazard.
PlaceForm.OnParametersSet's reseed paths overwrote reader-editable fields and theirIsDirtybaseline together, atomically. A reseed arriving after the reader typed or picked a value would have discarded their input and leftIsDirtyreadingfalse— no prompt, no signal. Worse than the behaviour task 209 fixed, which at least still reported dirty.Both reseed paths are covered, after an audit rather than a spot fix. The ticket named one; review found the second; the audit established there is no third.
SeedFrom(InitialCoordinates) — a second map click relocating an open add panel_latitude,_longitudeHome.razor'sLeafletMap.Inert="OverlayFocus.IsOpen"CoordinateFieldsMatchLastSeedSeedFromPartial(InitialValues) — a second import-repair seed (ADR-0135)_name,_description,_latitude,_longitudeImportReportBanner.razor'sinert="@OverlayFocus.IsOpen"InitialValueFieldsMatchLastSeedEdit mode never re-seeds:
OnParametersSetreturns early wheneverPlace is not null, so a changedPlaceafter mount is inert by construction.DefaultGroupId,GroupsandCallerRoledo not feed a seeded field.Decision: refuse to reseed, rather than reseed-and-stay-dirty. The ticket's bar was that silently winning is not an option; refusing means the reader's value is never overwritten in the first place. Neither guard needed new state — each re-derives "has the reader supplied something" from the existing seed baseline, the same comparison
IsDirtyalready makes, withNormalizeDescriptionon the description leg to match. Reusing it rather than adding a parallel "was this reader-supplied" flag follows ADR-0126's own reasoning for rejecting a duplicate field for the group selector. Both refusals are all-or-nothing across the fields the path writes, because both write them atomically.Reachability verified, not inherited. Traced on current
main:ModalOverlaycallsOverlayFocus.EnterAsyncsynchronously — adding to_openCallersand raisingChangedbefore anyawait— andExitAsyncon dispose; both consumers'inertbindings readOverlayFocus.IsOpen; no commit since ADR-0126 weakens any of it.Why now.
OverlayFocusTrapis the single shared mechanism behind both gates. Task 197 is an open, active investigation into exactly that trap's release timing — six documented failed attempts, ADR-0119 — and a fix there could move whenIsOpenflips for every consumer. That is the change the ticket warned "would arrive looking unrelated". Neither guard depends on the gate, so both stay correct however task 197 resolves.Watched to fail, per guard. Reverting the coordinate guard makes
LatitudeFieldValueread48.8566instead of the picked40.7128. Reverting the repair-seed guard makes the name read"Big Ben"instead of the typed value. Each restored, with 68/68PlaceFormTests, 916/920 WebUI (4 pre-existing skips) and the architecture suite green.What the tests do and do not prove. Both drive
PlaceFormdirectly through bUnit and assert the reader's value survives a reseed withIsDirtystill true — the repair-seed test also asserts a still-blank latitude survives, proving the refusal is atomic rather than per-field. They prove the guards work. They do not prove any browser path reaches these states, because none does.ADR-0126 is annotated per ADR-0170's correction shape at the Consequences paragraph that said the hazard was "not this ticket's to close"; its Decision section is untouched, since
IsDirty's formula did not change. ADR-0135 is deliberately not annotated: it explains the baseline reuse but never claimed the repair-seed path was safe against this hazard, so there is no false statement there to correct — the code and tests are the record.Verdict: changes needed
Verified independently against the pinned SDK (10.0.100): the guard is a correct reuse of
IsDirty's own coordinate comparison — traced throughIsDirty, confirmed the two predicates cannot diverge whileInitialCoordinatesis the active seed (mutually exclusive withInitialValues, so the coalescing collapses to exactly this pair). Reverted the guard to unconditionalSeedFromand reran the new test: it reddens exactly as claimed (LatitudeFieldValuereads48.8566not40.7128), restored and reconfirmed 67/67PlaceFormTestsgreen.AdrFiles_Corrected_HasAMatchingInlineMarkerInBothDirectionspasses; the correction sits in Consequences, Decision untouched, as claimed. Reachability trace checks out:OverlayFocusTrap.EnterAsyncdoes add-to-set-and-fire-Changedbefore its firstawait, matching the PR body's claim. No AI attribution, no stray ADR number, no American spellings in the diff.One real gap, inline:
SeedFromPartial's own reseed path (theInitialValues/import-repair seed, ADR-0135) has the exact same silently-discards-and-clears-IsDirtyshape, gated by the same sharedOverlayFocusTrapthis PR's own "why now" argument is about — and it's untouched and unrecorded. Given this project's own standard for not losing a found-but-unfixed hazard (task 210 exists for exactly this reason), that needs either the same guard or an explicit record before this merges.This branch —
SeedFromPartialfor theInitialValues/repair-seed path — has the identical unconditional-overwrite shape task 210 just closed forInitialCoordinates: a second reseed here would still silently discard_name/_description/coordinates the reader had already typed and clearIsDirtyin the same step. It's gated by the sameOverlayFocusTrap:ImportReportBanner.razorputs.import-report-banner(all its own Repair buttons)inert="@OverlayFocus.IsOpen", so it's exactly as unreachable today, and exactly as exposed to task 197's timing change, as the coordinate path this PR fixes. Left unguarded here and not named anywhere (ADR-0135 has no note of it). Either apply the sameCoordinateFieldsMatchLastSeed-style guard to this branch, or record it explicitly (a task-210-shaped ticket, plus an ADR-0135 consequence) so it isn't silently lost — the thing this whole ticket chain exists to avoid.Refuse a coordinate reseed once the reader has supplied their ownto Refuse a reseed once the reader has supplied their own valueVerdict: mergeable
Re-reviewed at
8247dea, superseding thechanges neededverdict at9086897.Completeness of the audit verified directly, not accepted on account: every write site of
_name/_description/_latitude/_longitude/_selectedGroupIdinPlaceForm.razor.csgrepped and checked — the only parameter-driven writes areOnInitialized's once-only seed (edit mode and create mode alike) and the twoOnParametersSetreseed branches, both now guarded;HandlePick/HandleCurrentLocation/HandleGroupChangedare all reader-driven, not parameter-driven. No@codeblock inPlaceForm.razorand no other lifecycle override exists, so there's no fourth place to check. "Edit mode never re-seeds" holds:OnParametersSetreturns before either branch wheneverPlace is not null.InitialValueFieldsMatchLastSeedre-derived againstIsDirty's own coalescing for theInitialValuespath (Place null, soPlace?.X ?? _seededInitialCoordinates?.X ?? _seededInitialValues?.Xcollapses to_seededInitialValues?.Xon every term) — exact match, includingNormalizeDescriptionon the description leg and the_name == (_seededInitialValues?.Name ?? string.Empty)handling of a seed whose ownNamewas null. No divergence.Mutated
InitialValueFieldsMatchLastSeed's guard back to unconditionalSeedFromPartialunder the pinned SDK (10.0.100): reddens exactly as claimed, name reads"Big Ben"not the typed value; restored and confirmed clean. The new test's own second assertion (latitude stays blank rather than picking up the new seed's51.5007) is what actually discriminates atomic-vs-per-field refusal, not just presence-of-a-guard — read the implementation to confirm it is a single&&-chain across all four fields, matching that assertion.Full local run, pinned SDK, solution built first:
dotnet build PlaceMark.slnxclean (0 warnings, 0 errors); 68/68PlaceFormTests; 916/920 WebUI (4 pre-existing, unrelated skips — notE2EFactAttribute); 148/148 architecture tests, includingAdrFiles_Corrected_HasAMatchingInlineMarkerInBothDirections. ADR-0126's own diff is unchanged from the previous round — Decision section untouched, correction still confined to Consequences.Atomicity (point 3): right call.
SeedFromPartialalready writes all four fields as one construction-time unit; a per-field refusal would need to track provenance per field independently — exactly the second-tracking state the whole design (reuseIsDirty's comparison, add nothing) was chosen to avoid — and would leave_seededInitialValuesdescribing no coherent single seed. The hypothetical "reader edited only the name, might want a fresh latitude" is a convenience nobody asked for, traded against new state and a real risk of the kind of drift task 210 exists to close.ADR-0135 (point 4): read fully. Its only reseed-related claim (Consequences, "dismissing the repair form and clicking 'Repair' again reseeds identically") is about a full unmount/remount after dismissal, proved by a different test entirely (
ImportReportBanner_RepairDismissedAfterTypingThenReopened...) — it says nothing about a second seed landing on an already-mounted, still-open instance. No false statement to correct; declining to annotate is correct.CI: local build/test run is the load-bearing verification here; run #888 for
8247deawas still in progress at review time with nothing in #887 (the stale, superseded run) to suggest a discrepancy.