Wayfarer
Conditional Place structure (BD2-08/09/10)
The supported Docker backend/Web and rewritten mobile client use the contracts below. They share the Place lifecycle owner and locked Segment reconciliation. These operations are transient compare/apply actions, without durable receipts, automatic rebasing, provider refresh or unknown-outcome recovery.
Routes and authority
Let B = /api/trips/{tripId}/conditional and
W = /api/trips/{tripId}/editor. All IDs are GUIDs.
| Operation | Bearer apply | Web apply | Prepare |
|---|---|---|---|
| Coordinates | POST B/places/{id}/coordinates |
Combined Web PUT below | POST at the apply URL plus /prepare |
| Same-Trip move | POST B/places/{id}/move |
Combined Web PUT below | POST at the apply URL plus /prepare |
| Place order | PUT B/regions/{id}/places/order |
PUT W/regions/{id}/places/order |
POST at the apply URL plus /prepare |
| Combined Web intent | No bearer authoring parity | PUT W/places/{id} |
POST W/places/{id}/prepare |
Bearer actions require the actual incoming active API credential and current
ownership; cookies do not supply bearer authority. Web actions require the User
cookie, ownership and antiforgery, including prepare. Authority is checked again
at apply’s final lock boundary. Missing/foreign targets are concealed equally.
The existing unconditional bearer Place PUT returns 409
mutation-originals-required; a complete unconditional Web draft is invalid.
Scalar Name/Notes/icon/color edits continue to use their existing conditional
PATCH with selected originals, without prepare or any structural fallback.
Scalar edits omit location intent and preserve absent or historical non-finite
stored locations. Editor reads, structural originals, prepare previews and
mutation responses project a historical non-finite Place location as null.
This public projection never repairs the stored point or clears enrichment;
private conditions retain raw geometry, distinguishing invalid geography from
absence and detecting drift even when both publicly project as null. Unrelated
Address edits remain permitted under the existing metadata policy. Explicit
provider authoring requires a finite effective location; invalid new coordinates
continue to reject with controlled validation errors.
Originals, preparation and explicit review
Prepare accepts the operation’s client originals and exact desired intent. It reads a coherent snapshot, compares public originals and returns:
{
"contract": "bd2-1",
"operation": "coordinates",
"tripId": "<trip-guid>",
"targetId": "<place-guid>",
"requiresReview": true,
"place": "<complete public material original, or null for order>",
"placesByRegion": {},
"affectedSegmentIds": [],
"conditionToken": "<opaque protected conditions>"
}
place above denotes the object defined below, not a literal string. The response
contains no raw private enrichment/provenance snapshots. Conditions bind the
server’s protection keys, owner, Trip, typed target, operation, exact desired
intent and complete material fingerprint. Coordinate, move and order purposes
are separate. Nondestructive conditions have no wall-clock expiry; current
material comparison remains decisive. They authorize neither access nor replay.
Prepare never replaces submitted originals. It rejects stale public evidence. Because the current client view does not contain original private dependencies, the prepared private base requires explicit review. Web presents that review before apply and preserves the authored draft on cancellation. A mobile client may acquire/review conditions before authoring the operation or explicitly review current consequences later; it must preserve an offline intent that lacks adequate original evidence until that review occurs. Do not silently treat a later prepare as the base against which the user originally edited.
Apply repeats the same request with conditionToken and reviewed: true added.
It discovers locks, takes the existing profile → Segment → Place → Region → Trip
order, rediscoveres the scope after waits and compares the final material state.
New dependencies, changed profiles/anchors, membership/order drift or stale target
content reject without applying any partial operation. No automatic retry changes
expected values or exchanges a stale token for a fresh one.
BD2-08: coordinates
Prepare and apply use this body, adding the reviewed conditions only for apply:
{
"expected": {
"regionId": "<original-region-guid>",
"location": { "latitude": 37.98, "longitude": 23.73 }
},
"location": { "latitude": 38.0, "longitude": 23.75 }
}
Original location may be null. Desired bearer coordinates must be a finite, inclusive WGS84 pair; zero is valid. Bearer coordinate clearing is not selected. The fingerprint binds original parent/location, the entire five-field enrichment tuple and affected routes, waypoint associations/positions/vertex indices, anchors, profiles, distance/duration/source and provenance. Unrelated scalar Notes are not coordinate dependencies. Coordinate-only intent preserves the current Address, including null, and does not contact providers.
Changed coordinates clear ResolvedFeatureName, ResolvedFeatureType, AddressEnrichmentProvider, AddressEnrichmentStorageMode and AddressEnrichedAt atomically. Unchanged coordinates retain all five. Existing reconciliation owns route vertex/fallback changes, waypoint effects, Wayfarer measurements, Manual duration preservation and provenance invalidation. Location, derived changes and current-safe Trip freshness commit together.
BD2-09: same-Trip move
A material original has every key below, preserving nullable stored values:
{
"id": "<place-guid>", "regionId": "<source-guid>",
"name": "Museum", "notes": null, "address": null,
"location": { "latitude": 38.0, "longitude": 23.75 },
"iconName": null, "markerColor": null, "displayOrder": null,
"resolvedFeatureName": null, "resolvedFeatureType": null
}
Notes use the published normalized authored representation; null and empty are
distinct originals. Web exposes exact nullable Address and stored order in
originalValues; do not substitute display defaults. Bearer reads expose these
values in their existing nested Place DTOs. The prepared fingerprint additionally
binds the complete persisted moved-Place content, including private enrichment.
{
"expected": {
"place": "<material original object>",
"placesByRegion": {
"<source-guid>": [{ "id": "<place-guid>", "displayOrder": null }],
"<destination-guid>": []
}
},
"destinationRegionId": "<destination-guid>",
"placeIdsByRegion": {
"<source-guid>": [],
"<destination-guid>": ["<place-guid>"]
}
}
Both Regions must already exist in the same owned Trip. Original sets include all members and their exact nullable orders; array order is immaterial. Desired vectors contain the complete union exactly once, moving only the selected Place. Foreign, duplicated, omitted or additionally transferred members reject. Only Region membership, both affected Place vectors and Trip freshness change. IDs, content, visits, enrichment, routes, measurements and provenance survive. Sparse, tied and null original orders remain evidence; normalization to 1..N affects only the two submitted scopes. Canonical Region identity/stored order stays untouched.
BD2-10: Place order
{
"expected": { "places": ["<every material original object in this Region>"] },
"placeIds": ["<every desired Place ID exactly once>"]
}
The complete original membership, nullable stored orders and every reordered Place’s material content are checked. Duplicate, foreign, omitted and stale vectors reject atomically. Only this Region’s Place orders and Trip freshness change; all unrelated fields/collections remain untouched. Bearer ordering may target owned Unassigned Places; Web ordering retains its canonical restriction (403). Neither action repairs or changes the canonical Region’s own stored order.
Combined Web intent
The Web PUT sends {expected, changes, placeIdsByRegion?, conditionToken,
reviewed}. Select only dirty name, notesHtml, iconName, markerColor,
address, location, regionId and explicit reverseGeocode intentions.
Without a move, expected.regionId and each intended field’s exact original are
required; coordinate intent includes original nullable location. With a move,
use the move’s complete expected.place, expected.placesByRegion and vectors;
put the desired destination in changes.regionId. It compares all applicable
originals and commits once, without PATCH followed by PUT.
placeIdsByRegion is forbidden without a move, including an empty map. Move
vectors must prove exactly the original source/destination membership within
the owned Trip; unrelated Regions or members reject before writes or affected
membership projection. This evidence is checked again under the apply locks.
Web retains explicit Address and metadata choices. Manual Address is trimmed, null means empty, and the established RetainCompatible policy clears the tuple when normalized Address or coordinates change. Explicit successful reverse geocoding replaces Address and the complete trusted tuple; it requires original Address/location evidence, freezes one provider result before domain locking and never repeats provider work on transaction contention. An unavailable requested provider uses the existing manual fallback/warning. Provider admission remains consumed if persistence fails. Ordinary coordinate/move/order actions make zero provider calls. No bearer Address/provider authoring capability is added.
Acknowledgements and holds
200 follows commit and returns contract: 'bd2-1', success, data, affected,
deletedIds and warnings; bearer additionally returns applied: true.
Place data is the complete editor Place DTO; order data is
{regionId, placeOrder}. Membership/order effects include every changed Place
and exact scoped vectors. Coordinate effects include complete affected Segment
DTOs with fresh route/aggregate conditions, Segment order and visit progress.
Acknowledgements are built inside the transaction before commit and adopted only
after validating the complete target/effect envelope. They are mutation effects,
not a full Trip baseline or a durable receipt.
Malformed/lost replies, disconnects, cancellation at commit and 5xx leave the attempt unknown. Newer drafts and corrective reversals survive acknowledgement; retired sessions cannot adopt late replies. Structural attempt facts and holds survive draft reset within the mounted workspace. A refresh, matching state, missing target or reset cannot establish noncommit or permit blind replay. There is no cross-reload recovery promise.
400 mutation-request-invalid rejects malformed originals/intent/vectors;
401 rejects invalid authority; concealed 404 does not prove deletion; 403 retains
protected-operation restrictions. Known-unapplied 409 categories are
mutation-review-required, mutation-dependency-stale, mutation-contention
and the rejected unconditional route’s mutation-originals-required. Keep the
draft and explicitly review an appropriate new base. Never infer noncommit from
an arbitrary network/HTTP failure or automatically retry structural intent.