{"schemaVersion":"mappls.journey-workshop.v1","slug":"ios-feedback-report-review","journeySlug":"ios-feedback-report-review","title":"Build iOS feedback report and review","summary":"An eight-lab, source-bounded workshop for the complete feedback report 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":"capture-feedback","stateModel":"hybrid","aggregate":"feedback report","actorCount":5,"stateCount":9,"transitionCount":9,"eventCount":9,"sourceGuideSlugs":["mappls-feedback-kit-ios-distribution","mappls-feedback-kit-ios-distribution-base","mappls-feedback-uikit-ios-distribution","mappls-feedback-uikit-ios-distribution-base"],"contractSlugs":[],"relatedTutorialSlugs":[],"sample":{"slug":"place-contribution-desk","name":"Place Contribution Desk","downloadPath":"/downloads/place-contribution-desk.zip","checksumPath":"/downloads/place-contribution-desk.zip.sha256","verifiedTestCount":8,"runCommand":"npm test --workspace @mappls-example/place-contribution-desk"},"labs":[{"slug":"model-lifecycle","title":"Model the lifecycle before the UI","duration":"15 min","objective":"Turn the feedback report blueprint into an explicit aggregate boundary owned by the application.","build":["draft: The host owns purpose, category intent, reporter consent, location context, and retention policy before opening provider UI.","capturing: One presented feedback generation owns UI, focus, permission prompts, callbacks, cancellation, and cleanup.","candidate: The adapter copied allow-listed category, bounded description, normalized location identity, and attachment references into application state.","submitted: The contributor deliberately committed one immutable application report with idempotency, expected version, audit, and outbox evidence.","provider_pending: A separately selected adapter has an acknowledged or unknown provider submission outcome that requires reconciliation.","review_pending: Submission evidence and any provider acknowledgement await an attributable quality, privacy, duplication, or policy decision.","resolved: An authorized reviewer recorded the disposition and exact supporting evidence, with provider resolution only when independently proven.","rejected: Review found the report invalid, duplicate, out of scope, or unsupported and preserved the reason plus evidence lineage.","cancelled: The contributor ended the active attempt before application submission and every UI resource was disposed."],"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_report by Host application: new aggregate → draft; emit feedback_report.created.","open_feedback_ui by Contributor: draft → capturing; emit feedback_report.ui_opened.","receive_candidate by Mappls Feedback Kit: capturing → candidate; emit feedback_report.candidate_received.","commit_submission by Contributor: candidate → submitted; emit feedback_report.submitted.","request_provider_submission by Provider adapter: submitted → provider_pending; emit feedback_report.provider_requested.","queue_review by Host application: submitted | provider_pending → review_pending; emit feedback_report.review_queued.","resolve_report by Operations reviewer: review_pending → resolved; emit feedback_report.resolved.","reject_report by Operations reviewer: review_pending → rejected; emit feedback_report.rejected.","cancel_report by Contributor: draft | capturing | candidate → cancelled; emit feedback_report.cancelled."],"prove":["create_report resolves to ios-feedback-report-review-feedback-report-created without claiming a provider webhook payload.","open_feedback_ui resolves to ios-feedback-report-review-feedback-report-ui-opened without claiming a provider webhook payload.","receive_candidate resolves to ios-feedback-report-review-feedback-report-candidate-received without claiming a provider webhook payload.","commit_submission resolves to ios-feedback-report-review-feedback-report-submitted without claiming a provider webhook payload.","request_provider_submission resolves to ios-feedback-report-review-feedback-report-provider-requested without claiming a provider webhook payload.","queue_review resolves to ios-feedback-report-review-feedback-report-review-queued without claiming a provider webhook payload.","resolve_report resolves to ios-feedback-report-review-feedback-report-resolved without claiming a provider webhook payload.","reject_report resolves to ios-feedback-report-review-feedback-report-rejected without claiming a provider webhook payload.","cancel_report resolves to ios-feedback-report-review-feedback-report-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":["Feedback report: Host-owned intent, lifecycle state, current immutable submission, and optimistic version. Keys: reportId, externalId, purpose, state, submissionRevision, version.","Submission revision: Allow-listed category, description, normalized location, consent, attachment references, and content identity. Keys: submissionId, reportId, contentHash, locationIdentity, consentVersion, submittedAt.","Provider attempt: Separately selected contract request, acknowledgement, unknown outcome, and reconciliation evidence. Keys: attemptId, submissionId, providerRequestId, status, receiptReference, lastCheckedAt.","Review decision: Attributable disposition against one exact submission and provider-evidence revision. Keys: reviewId, submissionId, reviewer, decision, reason, decidedAt."],"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 UI callback creates only a candidate; it cannot prove application submission, provider receipt, publication, or resolution.","One presentation generation produces at most one accepted terminal result and all late callbacks are ignored.","Every submission revision is immutable and content-hashed; rework creates a new linked attempt.","Provider acknowledgement and reviewer disposition are separate attributable records.","Credentials, view controllers, delegates, attachment bodies, and opaque provider payloads never enter durable state.","Idempotency, optimistic concurrency, audit, outbox, privacy purpose, retention, and redaction apply to every committed transition."],"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":["UI is denied, blocked, dismissed, or times out: detect with The active generation ends without a valid allow-listed candidate. Recover with Dispose, restore focus, retain the draft, record a safe reason, and offer a purpose-appropriate manual fallback.","A callback arrives after replacement or disposal: detect with Its presentation generation differs from the report's active generation. Recover with Ignore it, release its resources, and keep current report state unchanged.","Provider submission response is lost: detect with The application has a durable request identity but no authoritative acknowledgement. Recover with Remain provider pending and reconcile under the same identity before any retry.","Review rejects or requests corrected evidence: detect with Policy, privacy, duplication, location, or content validation fails for the immutable revision. Recover with Preserve the decision and original revision; create a new linked attempt rather than editing history."],"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 Place Contribution Desk 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":["UI open, activation, cancellation, candidate, and terminal outcome by released component version","Schema, size, stale-generation, duplicate, and late-callback rejection","Draft-to-candidate and candidate-to-submit conversion","Provider pending age, acknowledgement, unknown outcome, and reconciliation","Review queue age, disposition, rework, and duplicate rate","Idempotency replay, version conflict, audit, and outbox health","Privacy retention and redaction completion without attachment-body logging"],"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\" | \"capturing\" | \"candidate\" | \"submitted\" | \"provider_pending\" | \"review_pending\" | \"resolved\" | \"rejected\" | \"cancelled\";\ntype CommandName = \"create_report\" | \"open_feedback_ui\" | \"receive_candidate\" | \"commit_submission\" | \"request_provider_submission\" | \"queue_review\" | \"resolve_report\" | \"reject_report\" | \"cancel_report\";\n\ntype Command = {\n  name: CommandName;\n  aggregateId: string;\n  expectedVersion: number;\n  idempotencyKey: string;\n};\n\nconst transitions = {\n  \"create_report\": { from: [null], to: \"draft\", event: \"feedback_report.created\" },\n  \"open_feedback_ui\": { from: [\"draft\"], to: \"capturing\", event: \"feedback_report.ui_opened\" },\n  \"receive_candidate\": { from: [\"capturing\"], to: \"candidate\", event: \"feedback_report.candidate_received\" },\n  \"commit_submission\": { from: [\"candidate\"], to: \"submitted\", event: \"feedback_report.submitted\" },\n  \"request_provider_submission\": { from: [\"submitted\"], to: \"provider_pending\", event: \"feedback_report.provider_requested\" },\n  \"queue_review\": { from: [\"submitted\", \"provider_pending\"], to: \"review_pending\", event: \"feedback_report.review_queued\" },\n  \"resolve_report\": { from: [\"review_pending\"], to: \"resolved\", event: \"feedback_report.resolved\" },\n  \"reject_report\": { from: [\"review_pending\"], to: \"rejected\", event: \"feedback_report.rejected\" },\n  \"cancel_report\": { from: [\"draft\", \"capturing\", \"candidate\"], to: \"cancelled\", event: \"feedback_report.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_ios_feedback_report_review (\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_ios_feedback_report_review_commands (\n  aggregate_id text NOT NULL REFERENCES journey_ios_feedback_report_review(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_ios_feedback_report_review_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\":\"ios-feedback-report-review\",\"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\":\"ios-feedback-report-review\",\"scenario\":\"unknown-outcome\"}'\n\n# Reconcile feedbackreport 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_report\",\n  \"aggregateId\": \"fixture-ios-feedback-report-review-001\",\n  \"expectedVersion\": 0,\n  \"idempotencyKey\": \"cmd_ios-feedback-report-review_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=ios-feedback-report-review&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=ios-feedback-report-review&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=ios-feedback-report-review&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=ios-feedback-report-review&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=ios-feedback-report-review&scenario=unknown-outcome#lab"}],"acceptance":["All 9 reviewed transitions are implemented with actor and source-state checks.","All 9 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 Place Contribution Desk 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/ios-feedback-report-review/workshop","apiPath":"/api/journey-workshops?journey=ios-feedback-report-review","providerCalls":0,"writesExposed":false}