{"schemaVersion":"mappls.journey-workshop.v1","slug":"widget-selection-session","journeySlug":"widget-selection-session","title":"Build Application-owned widget selection","summary":"An eight-lab, source-bounded workshop for the complete widget selection session lifecycle: exact commands and events, durable records, replay, concurrency, unknown outcomes, hostile fixtures, a maintained capstone, and production exit evidence.","duration":"2 hr 10 min","level":"Advanced","productSlug":"app-widgets-deep-links","stateModel":"hybrid","aggregate":"widget selection session","actorCount":4,"stateCount":7,"transitionCount":8,"eventCount":8,"sourceGuideSlugs":["mappls-app-widgets","mappls-android-sdk","mappls-ui-widget-ios-distribution","mappls-ui-widget-ios-distribution-base","mappls-flutter-sdk","mappls-react-native-sdk"],"contractSlugs":[],"relatedTutorialSlugs":[],"sample":{"slug":"widget-journey-host","name":"Widget Journey Host","downloadPath":"/downloads/widget-journey-host.zip","checksumPath":"/downloads/widget-journey-host.zip.sha256","verifiedTestCount":8,"runCommand":"npm test --workspace @mappls-example/widget-journey-host"},"labs":[{"slug":"model-lifecycle","title":"Model the lifecycle before the UI","duration":"15 min","objective":"Turn the widget selection session blueprint into an explicit aggregate boundary owned by the application.","build":["draft: Host-owned address or place intent exists without an active provider surface or committed Mappls identity.","widget_open: One launch generation owns the provider surface, lifecycle callbacks, focus, cancellation, and timeout.","fallback_active: The provider surface is unavailable and a bounded manual or search-assisted host path remains operable.","candidate_received: An exact-origin or native-adapter result passed schema validation but is not yet a business selection.","selected: The user deliberately committed a normalized Mappls Pin and label against the current host draft version.","submitted: Host text and committed selection form one immutable, attributable business record with an outbox event.","cancelled: A named actor ended the journey without submission and with a recorded reason."],"prove":["Every persisted state exists in the reviewed blueprint.","Terminal states reject ordinary forward commands.","Recovery text is operational guidance, not another hidden state."]},{"slug":"command-event-contract","title":"Implement every command and event pair","duration":"20 min","objective":"Make intent, actor authority, allowed source state, committed state, and emitted fact reviewable together.","build":["create_session by Host application: new aggregate → draft; emit host.session_created.","open_widget by Application user: draft | fallback_active → widget_open; emit widget.opened.","activate_fallback by Platform adapter: widget_open → fallback_active; emit widget.fallback_activated.","receive_candidate by Platform adapter: widget_open | fallback_active → candidate_received; emit location.candidate_received.","accept_selection by Application user: candidate_received → selected; emit location.selected.","edit_host_text by Application user: draft | widget_open | fallback_active | candidate_received | selected → draft; emit host.text_edited.","submit by Application user: selected → submitted; emit host.submitted.","cancel by Application user: draft | widget_open | fallback_active | candidate_received | selected → cancelled; emit host.cancelled."],"prove":["create_session resolves to widget-selection-session-host-session-created without claiming a provider webhook payload.","open_widget resolves to widget-selection-session-widget-opened without claiming a provider webhook payload.","activate_fallback resolves to widget-selection-session-widget-fallback-activated without claiming a provider webhook payload.","receive_candidate resolves to widget-selection-session-location-candidate-received without claiming a provider webhook payload.","accept_selection resolves to widget-selection-session-location-selected without claiming a provider webhook payload.","edit_host_text resolves to widget-selection-session-host-text-edited without claiming a provider webhook payload.","submit resolves to widget-selection-session-host-submitted without claiming a provider webhook payload.","cancel resolves to widget-selection-session-host-cancelled without claiming a provider webhook payload."]},{"slug":"durable-records","title":"Persist restart-safe records","duration":"15 min","objective":"Separate business identity, provider evidence, command receipts, immutable facts, audit, and downstream delivery.","build":["Selection session: Current host draft, lifecycle state, launch generation, candidate, selection, and optimistic version. Keys: sessionId, externalId, state, version, launchGeneration, hostText.","Normalized selection: Application-owned portable place identity independent of provider UI lifetime. Keys: schemaVersion, mapplsPin, label, source, selectedAt, selectedBy.","Audit event: Attributable state transition and recovery history. Keys: eventId, aggregateVersion, type, actor, idempotencyKey, occurredAt.","Transactional outbox: Exactly-once-in-effect notification and downstream form processing. Keys: outboxId, eventId, status, attempts, nextAttemptAt."],"prove":["Process restart restores the same aggregate version and command result.","Opaque SDK or native UI objects are not durable records.","Provider evidence and application decisions remain distinguishable."]},{"slug":"concurrency-replay","title":"Make concurrency and replay deterministic","duration":"15 min","objective":"Apply optimistic expected versions and aggregate-scoped idempotency before executing effects.","build":["A provider callback or browser message creates only a candidate, never a submitted business record.","Every browser message matches the exact reviewed origin and a versioned allow-listed schema.","Only a six-character alphanumeric Mappls Pin and bounded printable label enter durable selection state.","A host-text edit invalidates every candidate and committed selection from the previous draft version.","One launch generation produces at most one terminal adapter outcome; late callbacks are ignored.","Credentials, provider controllers, native views, bridge objects, and opaque response payloads are never persisted."],"prove":["An exact replay returns the first result without another event or version.","A reused key with different intent conflicts.","A stale expected version changes no durable truth."]},{"slug":"effects-reconciliation","title":"Control effects and unknown outcomes","duration":"15 min","objective":"Commit outbox intent atomically, execute effects outside the transaction, and reconcile ambiguous results.","build":["Widget fails, is denied, or times out: detect with The active generation reaches a typed failure without a valid terminal candidate. Recover with Dispose it, restore focus, record the reason, and activate a useful host-owned fallback.","Message arrives from a wrong origin or with unknown fields: detect with Exact origin or narrow schema validation fails before domain processing. Recover with Reject without changing aggregate state and emit a safe rejection metric without storing opaque content.","Callback arrives after screen disposal or a newer launch: detect with Owner is inactive or result generation differs from the current session generation. Recover with Ignore the late result and clean up its provider resources without committing state.","User edits the address after selecting a place: detect with Host draft version changes while a candidate or selection exists. Recover with Clear both values, return to draft, and require a new selection before submission.","Submit response is lost: detect with Client lacks acknowledgement but retains session and idempotency identity. Recover with Repeat the same command key or read the session; never create a second business record."],"prove":["A timeout remains an unknown outcome until identity-based reconciliation completes.","Retries are bounded and preserve the original business and command identities.","Dead-letter or manual review retains the entire attempt history."]},{"slug":"hostile-scenarios","title":"Run all hostile fixture scenarios","duration":"15 min","objective":"Exercise the success path plus replay, concurrency, state, and response-loss failures without an account.","build":["Complete journey: Commit the shortest reviewed success path to the journey-specific operating target.","Idempotent replay: Repeat one command identity and prove that version, event identity, and side effects do not duplicate.","Stale version: Reject a command based on an outdated aggregate version without changing durable truth.","Invalid transition: Reject a known command when the current state does not permit it.","Unknown outcome recovery: Reconcile after a lost response, then replay the original command identity safely."],"prove":["All fixture checks pass for all five scenarios.","Rejected commands emit no event and do not increment version.","The fixture makes zero provider calls and exposes no write tool."]},{"slug":"maintained-capstone","title":"Trace the Widget Journey Host capstone","duration":"20 min","objective":"Follow the maintained source through domain rules, adapter seam, repository transaction, HTTP boundary, UI evidence, and restart test.","build":["Run the app's declared test suite (8 tests).","Run fixture mode without a credential.","Inspect audit and outbox evidence after each transition.","Restart the process and continue the same aggregate."],"prove":["The downloadable archive checksum verifies before execution.","The capstone covers the journey target without inventing provider completion.","Browser and HTTP surfaces report the same durable version."]},{"slug":"production-exit","title":"Qualify the real integration boundary","duration":"15 min","objective":"Replace only reviewed adapter seams and collect independent production evidence without weakening application invariants.","build":["Widget launch, time-to-active, and terminal outcome by platform and component version","Origin and schema rejection counts without raw payload retention","Fallback activation, completion, and abandonment rate","Candidate-to-selection and selection-to-submit conversion","Stale selection invalidation after host edits","Duplicate, late, and superseded callback count","Idempotency replay and optimistic version conflict rate","Outbox backlog, retry, and dead-letter age"],"prove":["Exact product entitlement and regional behavior are validated separately.","Provider contract tests cover success, rejection, throttling, timeout, and unknown outcome.","Security, privacy, operations, rollback, and product owners approve exact evidence.","Fixture completion is never presented as provider or production completion."]}],"codeSamples":[{"language":"typescript","label":"TypeScript aggregate boundary","code":"type State = \"draft\" | \"widget_open\" | \"fallback_active\" | \"candidate_received\" | \"selected\" | \"submitted\" | \"cancelled\";\ntype CommandName = \"create_session\" | \"open_widget\" | \"activate_fallback\" | \"receive_candidate\" | \"accept_selection\" | \"edit_host_text\" | \"submit\" | \"cancel\";\n\ntype Command = {\n  name: CommandName;\n  aggregateId: string;\n  expectedVersion: number;\n  idempotencyKey: string;\n};\n\nconst transitions = {\n  \"create_session\": { from: [null], to: \"draft\", event: \"host.session_created\" },\n  \"open_widget\": { from: [\"draft\", \"fallback_active\"], to: \"widget_open\", event: \"widget.opened\" },\n  \"activate_fallback\": { from: [\"widget_open\"], to: \"fallback_active\", event: \"widget.fallback_activated\" },\n  \"receive_candidate\": { from: [\"widget_open\", \"fallback_active\"], to: \"candidate_received\", event: \"location.candidate_received\" },\n  \"accept_selection\": { from: [\"candidate_received\"], to: \"selected\", event: \"location.selected\" },\n  \"edit_host_text\": { from: [\"draft\", \"widget_open\", \"fallback_active\", \"candidate_received\", \"selected\"], to: \"draft\", event: \"host.text_edited\" },\n  \"submit\": { from: [\"selected\"], to: \"submitted\", event: \"host.submitted\" },\n  \"cancel\": { from: [\"draft\", \"widget_open\", \"fallback_active\", \"candidate_received\", \"selected\"], to: \"cancelled\", event: \"host.cancelled\" },\n} as const;\n\nexport function decide(current: { state: State | null; version: number }, command: Command) {\n  const rule = transitions[command.name];\n  if (command.expectedVersion !== current.version) throw new Error(\"version_conflict\");\n  if (!rule.from.includes(current.state as never)) throw new Error(\"invalid_transition\");\n  return {\n    state: rule.to as State,\n    version: current.version + 1,\n    event: rule.event,\n    idempotencyKey: command.idempotencyKey,\n  };\n}\n\n// Persist the result, immutable event, audit row, and outbox intent atomically.\n// Store the first result by idempotencyKey before executing another effect."},{"language":"sql","label":"SQL durability skeleton","code":"CREATE TABLE journey_widget_selection_session (\n  aggregate_id text PRIMARY KEY,\n  state text NOT NULL,\n  version bigint NOT NULL CHECK (version > 0),\n  updated_at timestamptz NOT NULL DEFAULT now()\n);\n\nCREATE TABLE journey_widget_selection_session_commands (\n  aggregate_id text NOT NULL REFERENCES journey_widget_selection_session(aggregate_id),\n  idempotency_key text NOT NULL,\n  request_hash text NOT NULL CHECK (length(request_hash) = 64),\n  committed_version bigint NOT NULL,\n  result_json jsonb NOT NULL,\n  PRIMARY KEY (aggregate_id, idempotency_key)\n);\n\nCREATE TABLE journey_widget_selection_session_outbox (\n  event_id text PRIMARY KEY,\n  aggregate_id text NOT NULL,\n  aggregate_version bigint NOT NULL,\n  event_type text NOT NULL,\n  payload jsonb NOT NULL,\n  published_at timestamptz\n);\n\n-- In one transaction: lock aggregate, compare version, decide, append audit/event,\n-- insert the outbox row, and remember the exact command result."},{"language":"curl","label":"Complete fixture journey","code":"curl --request POST 'https://developer.mappls.com/api/journey-simulator' \\\n+  --header 'content-type: application/json' \\\n+  --data '{\"journey\":\"widget-selection-session\",\"scenario\":\"complete-journey\"}'"},{"language":"curl","label":"Unknown-outcome drill","code":"curl --request POST 'https://developer.mappls.com/api/journey-simulator' \\\n+  --header 'content-type: application/json' \\\n+  --data '{\"journey\":\"widget-selection-session\",\"scenario\":\"unknown-outcome\"}'\n\n# Reconcile widgetselectionsession identity and the original idempotency key.\n# Never mint a replacement key merely because the response was lost."},{"language":"json","label":"First command envelope","code":"{\n  \"command\": \"create_session\",\n  \"aggregateId\": \"fixture-widget-selection-session-001\",\n  \"expectedVersion\": 0,\n  \"idempotencyKey\": \"cmd_widget-selection-session_001\",\n  \"evidenceBoundary\": \"application-owned-workshop\"\n}"}],"simulationScenarios":[{"slug":"complete-journey","title":"Complete journey","outcome":"Commit the shortest reviewed success path to the journey-specific operating target.","href":"/tools/journey-lab?journey=widget-selection-session&scenario=complete-journey#lab"},{"slug":"idempotent-replay","title":"Idempotent replay","outcome":"Repeat one command identity and prove that version, event identity, and side effects do not duplicate.","href":"/tools/journey-lab?journey=widget-selection-session&scenario=idempotent-replay#lab"},{"slug":"stale-version","title":"Stale version","outcome":"Reject a command based on an outdated aggregate version without changing durable truth.","href":"/tools/journey-lab?journey=widget-selection-session&scenario=stale-version#lab"},{"slug":"invalid-transition","title":"Invalid transition","outcome":"Reject a known command when the current state does not permit it.","href":"/tools/journey-lab?journey=widget-selection-session&scenario=invalid-transition#lab"},{"slug":"unknown-outcome","title":"Unknown outcome recovery","outcome":"Reconcile after a lost response, then replay the original command identity safely.","href":"/tools/journey-lab?journey=widget-selection-session&scenario=unknown-outcome#lab"}],"acceptance":["All 8 reviewed transitions are implemented with actor and source-state checks.","All 8 application event identities are immutable and versioned.","Exact replay, idempotency conflict, stale version, invalid transition, and unknown outcome are tested.","Aggregate, event, audit, command result, and outbox intent commit atomically.","The Widget Journey Host capstone passes 8 declared tests after archive checksum verification.","Provider entitlement, payload, callback, completion, and production behavior remain independently evidenced."],"sourceBoundary":["The journey blueprint and application event contracts are implementation guidance, not Mappls provider payload specifications.","Only linked normalized contracts and source guides may define provider request syntax; empty evidence is never backfilled.","The simulator and maintained capstone operate in explicit fixture mode and make no entitlement claim.","Credentials, precise production payloads, opaque native objects, and provider secrets stay outside workshop inputs and durable examples."],"releaseBoundary":"Workshop completion proves an application-owned reliability design only. Production still requires issued entitlement, exact adapter contract tests, regional and quota validation, security/privacy review, operational drills, and independent release approval.","websitePath":"/journeys/widget-selection-session/workshop","apiPath":"/api/journey-workshops?journey=widget-selection-session","providerCalls":0,"writesExposed":false}