Wayfarer
Protected Region ordering (#853 BD2-11)
The coordinated backend/Web and rewritten mobile app use one complete, original-aware normal-Region vector. This operation has no child, canonical-order, scalar-content or historical repair write authority. Place deletion and Region-deletion integration remain the following batch; final C1/C2 reconciliation and C5 acceptance remain separate.
Routes and authority
| Caller | Prepare (read only) | Apply |
|---|---|---|
| Bearer | POST /api/trips/{tripId}/conditional/regions/order/prepare |
PUT /api/trips/{tripId}/conditional/regions/order |
| Web | POST /api/trips/{tripId}/editor/regions/order/prepare |
PUT /api/trips/{tripId}/editor/regions/order |
Bearer requires the actual incoming active credential and current owned Trip. Web requires an authenticated User cookie and antiforgery token on both routes. Both apply paths recheck current account, ownership and scope after lock waits; bearer also revalidates the incoming credential. Sorted Region locks precede the Trip lock, preserving the lifecycle lock order without acquiring child locks. Apply uses Read Committed with final complete membership/material rediscovery under those locks; preparation reads a Repeatable Read snapshot. Normal creation, scalar and lifecycle writers coordinate through these same Region/Trip barriers.
Original evidence and example
Capture the full authoritative starting view before authoring the order. Normal
Region originals contain every id, actual displayOrder, name, nullable
normalized notes, exact nullable coverImageUrl, and nullable
center:{latitude,longitude}. Canonical Unassigned Places is identified by its
exact stored name regardless of stored order. Its separate original is either
null (explicit absence) or {id,displayOrder}. Do not send canonical content or
its ID in the normal vector. Children are preserved, not content dependencies.
Example mobile preparation body (two normal Regions, historical canonical order):
{
"expected": {
"regions": [
{
"id": "10000000-0000-0000-0000-000000000001",
"displayOrder": 4,
"name": "Athens",
"notes": null,
"coverImageUrl": null,
"center": { "latitude": 37.98, "longitude": 23.73 }
},
{
"id": "10000000-0000-0000-0000-000000000002",
"displayOrder": 8,
"name": "Thessaloniki",
"notes": "<p>Station</p>",
"coverImageUrl": "https://example.test/station.jpg",
"center": null
}
],
"canonical": {
"id": "10000000-0000-0000-0000-000000000003",
"displayOrder": 17
}
},
"regionIds": [
"10000000-0000-0000-0000-000000000002",
"10000000-0000-0000-0000-000000000001"
]
}
Prepare returns contract:"bd2-1", operation:"region-order", tripId,
requiresReview:true, the matching original regions/canonical and an opaque
conditionToken. Validate scope and echoed originals, explicitly review the
operation, then PUT the same expected/vector body with the returned
conditionToken and reviewed:true. Never replace stale client originals with a
fresh preparation response. No original proof means retain/hold the draft until
the user explicitly reviews a fresh starting view. Public full-read coordinate
arrays are [longitude,latitude]; convert them to the named pair above. Web uses
its published nullable originalValues, not display Notes/cover defaults.
Prepared conditions bind owner, Trip, Web/bearer dialect, exact intent and raw
normal-Region material under the distinct
Wayfarer.RegionOrder.Conditions.v1 purpose. Place and deletion proofs cannot
substitute. Preparation rejects stale public originals rather than adopting a new
base; raw content is rechecked at final apply. Tokens are transient conditions,
without a durable receipt, terminality or replay guarantee.
Atomic result and rejection
A successful apply writes normal DisplayOrder values 1..N in desired sequence
and current-safe maximum Trip freshness in one transaction. It preserves canonical
stored order, every Region scalar, children, Place/Area orders, Segment routes,
waypoints, provenance, saved measurements, visits and unrelated Trip metadata.
An empty normal vector is valid only for an actually empty normal set. Existing
normal sparse/tied/negative orders are compared exactly before the explicitly
requested vector replaces them; no other order normalization occurs.
Both return contract:"bd2-1", success:true, data:{tripId,regionOrder}, and
affected.regions containing exactly the updated normal Region DTOs, including
published originals. affected.regionOrder and data.regionOrder are the same
complete resulting Region sequence, sorted by actual stored order then ID,
including canonical at its preserved stored position. All other affected slices
are empty/null, deleted IDs are empty and warnings are empty. Bearer additionally
returns applied:true. These results describe committed effects only.
| Status | Meaning |
|---|---|
400 mutation-request-invalid |
Malformed/missing/duplicate fields or IDs, incomplete vectors, or canonical/foreign members inconsistent with submitted originals. No partial writes. |
| 401 | Invalid authority, including credential/account invalidation observed after waiting. |
| 404 | Missing/foreign Trip scope, concealed before request parsing. |
409 mutation-base-stale |
Changed membership, stored orders, material content or canonical identity/order; no current values are disclosed. |
409 mutation-review-required |
Missing explicit review/prepared conditions. |
409 mutation-contention |
Proven rolled-back PostgreSQL transaction abort before commit begins. |
A valid vector followed by a Notes edit can both succeed. A material Notes edit first makes a previously retained order stale, symmetrically for Web/bearer. Failed apply rolls back orders and freshness together. Commit, rollback, transport or response uncertainty is never relabeled a definitive no-effect conflict.
Web acknowledgement and participating writers
The mounted list pins material and stored orders at drag start, before prop refresh can replace them. The Region owner retains that base before prompts/awaits, uses the protected path, and validates returned Trip, identities, complete vector, material preservation and unrelated empty effects before parent adoption. It does not reset or acknowledge newer scalar intent. Retired sessions publish no effects. Unknown apply outcomes retain the submitted order and a dirty navigation hold through refresh/reset/session retirement, prevent blind replay and false Saved, and explain explicit page abandonment. Ordinary refresh does not establish noncommit. No cross-reload recovery is promised.
Cover display follows the existing mapper: null, empty and whitespace-only stored
URLs project as coverImage:null; other URLs project unchanged. Acknowledgement
validation checks that display projection separately from exact stored
originalValues.coverImage comparison. Blank originals remain distinct from null
and from other whitespace; no cover storage is normalized by reordering.
Unconditional Web {regionIds} bodies reject. Individual bearer Region PUT cannot
change an ordinary Region’s stored order, even in a mixed scalar payload; identical
submitted orders remain no-ops and are never assigned. Existing canonical identity
protection remains. Other scalar/deletion coverage remains owned by its existing
operation and later reconciliation; this batch does not claim global all-writer
acceptance.
Focused evidence
RegionConditionalPostgresTests.Ordering.cs exercises paired valid operations,
reciprocal Notes/order sequences, invalid vectors, normal/canonical drift, rollback,
current authority and real Region lock waits. A routed creation queued behind
reorder rejects its stale append base without a ghost Region. Fresh PostgreSQL
rows/versions prove populated children, canonical storage and unrelated root
metadata are preserved. The existing Region scalar and creation qualification is
reused outside the changed boundary.
tests/client/regionConditionalEditing.test.mjs mounts the production Region owner
with Vue scheduling and covers delayed/newer/retired replies, scope/material/effect
validation, definitive rejection and lost/malformed acknowledgement holds. These
are routed server and mounted-client executions, not browser, native mobile or
production qualification. No database migration is required.