Software Design Reference Capstone · Strong packet

Capstone · Strong packet

The Returns-Resolution Packet, Worked

One initiative, fourteen artifacts, one line of reasoning running through all of them. This is what a lead-level packet looks like when every page exists to serve a decision.

Mission tie-in: this packet is the course's proof of judgment. It joins the Phase IV initiative artifacts (lessons 0039–0050) to the Phase V system-design artifacts (lessons 0051–0060) into a single, coherent whole.

How to read this packet

The initiative is the one used throughout Phase IV and Phase V: a retailer wants self-serve returns. The brief that arrived was “Build a self-serve returns portal by Q3. It needs automated approvals, carrier labels, and status notifications.” What follows is what a competent tech lead, working with a product manager and a product designer, actually produced over fourteen weeks — including the parts that turned out to be wrong.

Three reading habits will get the most out of it.

  1. Follow the “Traces to” line under each artifact. Every artifact here earns its place by serving an outcome, an assumption, a driver, a risk, or a rollout decision. If you cannot say which, the artifact is decoration.
  2. Read the connecting prose, not only the tables. The tables are the output. The paragraphs between them are the reasoning, and the reasoning is the part that transfers to your own initiative.
  3. Look for the costs. Three options are compared and two are rejected in writing; two decisions changed under evidence; one dissent is recorded and never resolved in the dissenter's favour; the outcome is met on one measure and missed on another. A packet with no visible cost is a packet with a hidden one.

The numbers in this packet belong to the case. They are internally consistent and each carries a source line, because a source line is the habit being taught — but they are a teaching fixture, not industry data. Do not cite them outside this course. Names identify roles inside the case; they are fictional.

Contents · the fourteen artifacts and the decision each one serves
#ArtifactThe decision it exists to serve
1Initiative frame (v0.3, with history)Is this worth doing, for whom, and what counts as done?
2Assumption mapWhat might be false, and what will we spend to find out?
3Current and target workflowWhere does the time actually go, and which waits can leave?
4Story map and release slicesWhat is the smallest end-to-end path that teaches us something?
5Acceptance examplesWhat exactly must be true — including when things fail?
6Architecture driversWhich qualities are allowed to shape the structure?
7C4 context, container, dynamic viewsWhat is ours, what runs, and what happens in time?
8State machine and contract reviewWho may change which fact, and what does a retry mean?
9Option comparison and ADR 0003Which mechanism, at what accepted cost, revisited when?
10Walking skeleton and delivery mapIn what order do we retire risk, and what can slip?
11Team charter and decision rightsWho decides, who advises, and what reopens a decision?
12Test, telemetry and rollout planHow do we learn in production without betting every customer?
13Week-3 initiative reviewWhat did the evidence change?
14CloseoutDid it work, who owns it now, and what did we remove?

Compare against the weak packet once you have read this one, then read the reviewer notes for the side-by-side judgment and the rubric dimension each difference touches.

1 · Initiative frame

The brief named a solution (“a portal”), a deadline (“Q3”), and three features. It named no user, no measure, and no baseline. The frame's job is to convert that into something a stranger can argue with. Everything downstream in this packet is scored against the outcome and guardrails below; if you change nothing else about how you work, change this.

Initiative frame · returns resolution · v0.3 · 2 October · owner: Priya (product)
Why now
Returns are the second-largest source of support contact volume, and returns volume rises about 40% from mid-November. No support headcount is approved for the season. Doing nothing means the contact queue grows with the returns queue and agents spend the season on status chasing instead of exceptions.
Problem
For signed-in domestic customers returning an unopened item within 30 days, a request waits a median of 72 hours for an agent decision, and 41% of those requests produce at least one support contact. Baseline source: returns queue export joined to the contact log, 1 June – 31 July, n = 4,812 requests; query owned by Priya, re-runnable.
Outcome
Move median time from request submitted to decision communicated for the eligible domestic cohort from 72 hours to under 4 hours, and the support-contact rate for that cohort from 41% to 31% or lower (a 25% relative reduction), with the pilot open to real customers by 30 September.
Guardrails
Fraud loss per 1,000 eligible returns at or below the 2.4 baseline (owner: Risk). Manual review queue at or below 50 requests/day (owner: Nils, support operations). WCAG 2.2 AA on the four critical flows (owner: Marco, design). Carrier cost per resolved return at or below the $3.42 baseline (owner: Priya). Personal data retained no longer than 30 days (owner: Ola, privacy).
First boundary
Signed-in domestic customer, unopened item, delivered 30 days ago or less, refund to the original payment method. Later expansion to opened/damaged items and to guest orders must remain possible without changing the eligibility contract — see contract review.
Non-goals
Exchanges. International shipping. Guest checkout. Redesigning warehouse intake. Replacing the agent tool. Each is here because someone plausibly assumed it was in scope.
Constraints
The order platform stays authoritative for order truth. The carrier contract is fixed for the pilot term. No payment card data enters the returns module. Personal data is retained for 30 days. The team is seven people and does not grow. The 30 September pilot date is fixed by the season, not by preference.
Assumptions
Seven named beliefs with impact, evidence strength, test and owner — see the assumption map. Two have since been falsified and one remains inconclusive.
Risks
R1 automatic decisions raise fraud loss (high × medium; early signal: fraud rate in the exposed cohort versus control; mitigation: hold rule plus staged exposure; owner: Risk; review weekly). R2 the referred cohort reads a referral as a refusal and contacts support anyway, harming the very measure (high × medium; signal: contact rate within the referred segment; owner: Marco). R3 the carrier cannot meet the label latency budget (medium × high before the spike, resolved — see ADR 0003). R4 guest orders are 29% of demand and stay outside the pilot, so the headline outcome overstates the business effect (medium × high; owner: Priya; decision date 15 November).
Decision rights
Outcome, scope and cohort: Priya. Experience: Marco. Architecture and technical risk: Dana. Rollout advance and reversal: Dana with Nils, stop authority to any on-call engineer. Full table in the team charter.
Next evidence
Comprehension retest at n = 12 (Marco, 15 September). Fraud rate in the 1% cohort versus matched control after 120 exposed requests (Risk, from 29 September). Guest-order opportunity sizing (Priya, by 15 November).

A frame that never changes is a frame nobody consulted. This one changed twice, and both changes are visible rather than quietly rewritten — which is what lets a newcomer in week 10 reconstruct why the outcome is worded the way it is.

Frame version history · what changed and why
VersionWhat changedWhy — the evidence that forced it
v0.1
25 August
First frame written from the brief in a 90-minute session with product and design. Outcome stated as “resolve eligible returns in under 4 hours and generate fewer support contacts.” Contact reduction had no number and no denominator. Success was described as “automatic approval for at least 90% of eligible requests.” Nothing yet — this is the honest starting position. It is recorded rather than deleted so the team can see how far the framing moved on evidence rather than on opinion.
v0.2
4 September
Three changes. (a) The contact target gained a number and a denominator: 41% → 31% for the eligible domestic cohort. (b) The outcome event changed from “automatic approval” to “decision communicated” — a referral to an agent within four hours now counts as success. (c) Added the non-goal “replacing the agent tool” and the guardrail “carrier cost per resolved return.” The 200-case policy sample (assumption A1) showed 15% of eligible requests are genuine judgement calls and 22% need a rule the pilot will not build. An outcome defined on automation would have scored the correct product behaviour — routing a judgement call to a human quickly — as failure, and would have pushed the team to automate decisions it should not automate.
v0.3
2 October
Two changes. (a) Guest orders moved from “non-goal” to “deferred opportunity, sized, decision date 15 November,” with the 29%-of-demand figure written into the frame. (b) Mobile comprehension work moved ahead of the evidence-photo rule in the slice order. Guest orders were re-argued in three consecutive weekly reviews because “non-goal” read as “refused forever.” Naming the deferral with a date and an owner ended the recurring debate at a cost of one line. The mobile reorder followed the week-1 exposure reading of an 11-point completion gap on mobile.

Traces to Nothing — this is the root. Every other artifact in this packet traces here. The test of the frame is the reverse direction: name any artifact below and you should be able to reach this page in one hop. Method: lesson 0039; template in the initiative playbook.

2 · Assumption map

The frame contains beliefs the team cannot yet defend. Writing them down is easy; the leadership work is deciding which ones are worth buying evidence for, and choosing a test proportionate to what the answer is worth. Impact-if-false runs vertically, evidence strength horizontally; the top-left corner — high impact, weak evidence — gets the budget.

                    EVIDENCE STRENGTH
                weak                    strong
              +-------------------------+-------------------------+
      high    |  A1 policy codifiable   |  A8 customers prefer    |
              |  A2 customers trust it  |     self-service to     |
  IMPACT      |  A3 carrier is fast     |     calling             |
  IF FALSE    |  A6 fraud stays flat    |                         |
              +-------------------------+-------------------------+
      medium  |  A5 agents accept the   |  A4 receipt events are  |
              |     refusal reasons     |     reliable            |
              |                         |  A7 policy changes often|
              +-------------------------+-------------------------+
              tested first: A1, A2, A3        no test bought
  
Assumption map · impact, evidence, chosen test, result
IdAssumptionRisk typeImpact if falseEvidence beforeTest chosen (cost)OwnerResult
A1 At least 90% of eligible requests match a codifiable policy rule. Feasibility High — the manual review queue guardrail (50/day) breaks, and the whole automation premise weakens. Weak: one estimate from the policy owner (Anya), no sample. Classify 200 closed agent decisions from June–July into codifiable / needs one new rule / judgement call. 2 analyst-days. Priya Falsified. 63% codifiable today, 22% need the opened-item photo rule, 15% are judgement calls. Automatic-decision coverage is 63% in the pilot's scope, 85% with the photo rule.
A2 Customers trust an automatic decision enough not to contact support about it. Value High — the contact-rate half of the outcome fails even if the latency half succeeds. Weak: inference from the 41% contact rate, no direct test. Five-participant moderated comprehension test on a clickable prototype of the accept, refuse and refer states. 3 designer-days. Marco Inconclusive. 2 of 5 read “your label is being prepared” as a refusal. n = 5 cannot separate signal from participant. Retest at n = 12 scheduled 15 September; the wording change shipped anyway because it is cheap and reversible.
A3 The carrier creates a label fast enough to sit inside the request that accepts the return. Feasibility High — it decides whether label creation is synchronous, which decides the failure model of the whole flow. Weak: a latency figure in the vendor's own documentation, never measured by us. Four-hour timeboxed spike against the carrier sandbox: 200 sequential creates measuring p50/p95/p99, then 20 induced timeouts retried with and without a client reference. Throw the harness away. Dana Falsified. p50 640 ms, p95 1.9 s, p99 11.4 s. 2 of 20 timeout-retries without a client reference produced a second label. Changed the decision — see ADR 0003.
A4 Warehouse receipt events are reliable enough to trigger a refund automatically. Feasibility Medium — a missed event strands money; a duplicate event double-refunds. Moderate: the event stream has six months of history and an existing consumer. Replay 30 days of receipt events against order ids; count missing, duplicate and late. 1 engineer-day. Lea Confirmed with a caveat. No missing or duplicate events; 0.4% arrive more than 24 hours after the physical scan. The refund SLA therefore starts at the event, not at the scan — written into the customer wording.
A5 Support agents will accept the system's refusal reasons rather than override them. Viability Medium — silent overriding means the policy model is wrong and nobody finds out. Weak: agent opinion in the discovery workshop. Shadow ten refusals with two agents; record every override and its stated reason. 1 day. Nils Partial. 3 of 10 overridden — all three for gift orders, which no rule covers. Produced one new rule, deferred to slice 3 rather than smuggled into the pilot.
A6 Fraud loss does not rise when the decision becomes automatic. Viability High — a guardrail breach stops the initiative regardless of the outcome measures. Weak, and untestable before exposure: fraud responds to the presence of the new path, not to a prototype. No pre-test bought. Converted into a rollout guardrail instead: exposed cohort versus matched control, stop threshold, named authority. See the rollout contract. Risk Deferred to rollout by design. Held at every stage; see closeout for the number and for what the sample size can and cannot detect.
A7 Policy rules change often enough to justify a separate, independently releasable policy module. Feasibility Medium — if false, the modifiability driver is imaginary and the module seam is unearned cost. Moderate: an existing policy change log nobody had read. Read the change log. 2 hours. Dana Confirmed. Nine policy changes in twelve months, four of them mid-quarter. Justifies driver D4 and the eligibility module seam.
A8 Customers would rather self-serve than call, for this class of request. Value High Strong: 71% of the contacts in the baseline join are status chasing on an already-submitted request, not requests for help deciding. None bought — deliberately. Buying evidence for a belief that is already well evidenced is a way of looking busy. — Held as a stated premise, re-checked in the closeout.

Two habits in that table are worth naming. First, the test is chosen to fit the decision, not the assumption: A3 got a four-hour spike because a number changed a structural choice, while A7 got two hours of reading because the log already existed. Second, “no test” is a legitimate, recorded choice — A6 could not be answered before exposure, so it became a rollout guardrail with a stop threshold, and A8 was already evidenced. A map where every row has a test is usually a map nobody costed.

Traces to Outcome (A1, A2, A8) · guardrails (A5, A6) · driver D1 and D4 (A3, A7) · rollout stop thresholds (A6). A1 and A3 each changed an artifact downstream; the change is visible in frame v0.2 and ADR 0003. Method: lesson 0040.

3 · Current-state and target workflow

The team mapped one real, recently completed case with a support agent, the policy owner, a warehouse clerk and two engineers in the room — events in past tense first, then the commands that caused them, then the policies, then the waits. Mapping the current state first is not ceremony: three of the five hotspots below were invisible to everyone who had only ever seen the target-state slide.

Current-state timeline · one real case, 4 August, request R-88412
Domain event (past tense)CommandActorPolicy that firedExternal systemElapsed
Return requestedEmail supportCustomer—Email / helpdesk0:00
Order verifiedLook up orderAgent—Order platform+2:10 (queue wait)
Evidence requestedAsk for photoAgent“If the box is open, ask for a photo”Email+2:20
Evidence receivedReply with photoCustomer—Email+19:40 (wait)
Policy interpretedRead policy pageAgentWindow, condition, payment-method rulesWiki+51:05 (queue wait)
Eligibility decidedApproveAgent“Within 30 days and unopened → approve”Helpdesk macro+51:20
Label createdBuy labelAgent—Carrier portal (manual)+51:35
Customer notifiedSend labelAgent—Email+51:40
Parcel receivedScan parcelWarehouse clerk“Match item to the return note”Warehouse system+6 days
Refund authorisedApprove refundFinance clerk“Refund after inspection passes”Spreadsheet → payments+6 days, twice-daily batch
Refund settled—Payments (external)Card scheme timingPayments+3–5 banking days

Two facts fall out of that table immediately. 68 of the 72 baseline hours sit in two queue waits — order verification and policy interpretation — and neither is work; both are waiting for a human to reach the request. And the evidence round-trip (19 hours) only exists for opened items, which the pilot scope excludes. That is why the pilot's narrow scope is not timidity: it removes the second-largest wait by construction.

Hotspots · where language, rules, ownership or confidence break down
HotspotExample / counterexampleWho knowsOpen questionNext evidence
“Approved” means three different thingsSupport: policy eligibility passed. Warehouse: inspection passed. Finance: money settled. A single global status would collapse all three.Anya, Nils, finance clerkAre these three states of one thing, or three things?Modelled as three owners with separate facts — see state and contracts.
Policy interpretation varies by agentThree agents given the same 31-day case: two refused, one approved “because the customer was polite about it.”AnyaHow much of the policy is actually written down?Assumption A1 sample — 63/22/15 split.
Gift orders have no rule and no ownerThe purchaser is not the recipient; refunds to the original payment method surprise the recipient.NilsWho decides gift-order policy?Owner assigned (Anya); rule deferred to slice 3 (A5).
Warehouse-to-refund handoff is a spreadsheetThe clerk copies order ids into a shared sheet; finance reads it twice a day. One id was transposed in the sampled month.Warehouse clerkIs the receipt event trustworthy enough to replace the sheet?Assumption A4 replay — reliable, 0.4% late.
Nobody owns “the request that went quiet”A request with no reply after the evidence email simply ages. No queue, no alert, no age limit.Nobody — that is the findingWhat is the maximum age of an open return?Became invariant candidate I4 and the 21-day expiry transition.
Vocabulary · one meaning per term, per model
TermPrecise local meaningOwned byPhrases to avoid
Eligibility decisionAccepted, refused with a reason code, or referred to an agent. Exactly one per return.Eligibility“approved”, “status 2”
ReferralA decision that a human must make this call, made within the same latency budget as an acceptance.Eligibility“pending”, “stuck”, “rejected”
Label intentA recorded promise to obtain exactly one carrier label for one accepted return. Exists before the carrier is called.Return journey“the label” (before it exists)
ReceiptThe warehouse's fact that the parcel matching this return arrived. Not an inspection result.Warehouse (external)“approved”, “received OK”
SettlementMoney has left us, as asserted by Payments. Only Payments may assert it.Payments (external)“refunded” used loosely

The target workflow is not the current one with automation bolted on. Two waits are deleted, one manual step is deliberately kept, and one new state (referral) is added because the evidence said 15% of cases need a human and pretending otherwise would have produced bad automatic refusals.

Target-state timeline · pilot scope
EventCommandActorPolicyBudgetChange from current
Return requestedSubmitReturn (idempotent)CustomerOne open return per order line—Structured, not email
Order verified—Returns moduleOrder platform is authoritativep95 200 msWait removed (was 2:10)
Eligibility decidedAssessEligibilityPolicy engineWindow, condition, payment method, fraud holdp95 800 msWait removed (was 49h)
  ↳ acceptedRecordLabelIntentReturn journeyExactly one intent per returnsame transactionNew — see ADR 0003
  ↳ referredQueueForAgentReturn journeyAgent answers within 4 h4 h SLANew state — 15% of cases
  ↳ refused—EligibilityReason code + human text + appeal path—Reason is now structured
Label createdCreateLabel (async, idempotent)Label worker → carrierOne carrier label per intent99% within 15 min of recoveryOff the request path
Customer notifiedNotifyCustomerReturn journeyOne notification per state change—Proactive, not on request
Parcel receivedRecordReceiptWarehouse clerk (manual for pilot)Item must match the return—Kept manual on purpose
Refund requestedRequestRefundReturn journeyOnly after receipt; never sets settlementwithin 24 h of receiptSpreadsheet removed
Refund settled—Payments (external)Refund ≤ captured amount — enforced by Payments3–5 banking daysUnchanged, and deliberately so

The manual seam, stated with its removal trigger. Warehouse receipt stays a human confirmation in the existing tool for the whole pilot, even though assumption A4 says the event stream is reliable. Reason: A4's replay is retrospective, and a wrong automatic refund is money we cannot claw back cheaply. Removal trigger: 14 consecutive days in which fewer than 0.1% of receipt events arrive late and zero mismatches are recorded. Owner: Lea. Capacity: about 40 confirmations/day, inside the clerk's existing workload.

Finally, the workflow yields candidate model boundaries — not deployables. Language, change cadence, invariants and ownership all point the same way, which is the only kind of boundary evidence worth acting on.

Candidate boundaries and the evidence for each
BoundaryLanguage evidenceChange evidenceInvariant evidenceOwnership evidence
Eligibility“Decision”, “reason”, “override” exist nowhere else9 policy changes/yr (A7)Exactly one open decision per returnAnya is the single named policy owner
Return journey“Intent”, “progress”, “notification”Changes with wording and channel, on design's beatOne label intent per accepted returnMarco owns the customer-visible steps
Carrier (external)Vendor vocabulary: shipments, referencesVendor's release cadence, not oursOne carrier label per intentContractual; not ours to change
Payments (external)“Settled”, “captured”Payment rail cadenceRefund ≤ captured amountRavi's team; financial truth

Traces to Outcome (the two deleted waits are the 68 of 72 hours) · assumption A1, A4, A5 (hotspots that became tests) · driver D2 and D4 (the boundary evidence) · risk R2 (the referral state). Method: lessons 0041, 0052, 0053.

4 · Story map and release slices

The backbone is the customer's journey, left to right. The depth beneath each activity is variation, not tasks. The release line is drawn across the map so that each slice is a complete journey for a narrower population — never a horizontal layer.

Story map · backbone across, variation down
Start requestCheck eligibilityChoose resolutionReturn parcelTrack outcome
signed-in orderdomestic / unopened / ≤30 daysrefund to original methodcarrier label, drop-offstatus page + email
gift orderreferred to agent (judgement)refund with restocking feeQR drop-off (no printer)SMS
guest orderopened / damaged, photo evidenceexchangehome collectionagent contact / appeal
agent-created on behalfinternationalstore creditin-store drop-offproactive delay notice
Release slices · what each one buys
SliceScopeWhat it testsExit evidenceTarget
Pilot slice (row 1) Signed-in, domestic, unopened, ≤30 days, refund to original method, carrier label, status page and email. Warehouse receipt confirmed manually. Assumption A2 (do customers trust an automatic decision?) and both halves of the outcome, on 1% of eligible demand. Median resolution and contact rate for the exposed cohort against a matched control, at n ≥ 120 requests; fraud and queue guardrails held. Open 29 September
Slice 2 Referral made first-class: agent decision view, four-hour referral SLA, appeal path after a refusal, referral-specific customer wording. Assumption A5 (will agents accept the reasons?) and risk R2 (does a referral read as a refusal?). Override rate below 15% of referrals; contact rate inside the referred segment no worse than the accepted segment plus 10 points. Week 3 of exposure
Slice 3 Opened and damaged items with structured photo evidence; the gift-order rule found by A5. The remaining 22% of A1 — does the photo rule actually make those cases codifiable? Automatic-decision coverage rises from 63% to at least 80% of in-scope requests without a fraud-guardrail move. After the pilot's continue decision
Deferred Guest checkout (29% of demand; needs an identity design and a separate fraud model — decision date 15 November). Exchanges. International. Store credit. Home collection. In-store drop-off. Nothing yet. Each carries the reason it waits, so the deferral is a decision rather than an oversight. Guest orders: sizing and fraud-model sketch from Priya and Risk by 15 November. —

Note what the pilot slice is not. It is not “the eligibility service”, and it is not “the returns database”. Both would be finishable without a single customer being served. The slice is narrow in variation — one cohort, one condition, one resolution — and complete across the journey, which is the only shape that can produce the outcome measure the frame is scored on.

Traces to Outcome (the pilot slice is the smallest thing that can move both measures) · assumptions A2, A5, A1 (one slice per open question) · risk R2 and R4 (slice 2 and the deferral) · rollout stage 3 (the 1% cohort receives exactly this slice). Method: lesson 0042.

5 · Acceptance examples

These were written before implementation, by product, design, engineering and the support-operations lead together. They are not a test plan; they are the specification of the rules the team argued about. Each rule carries a happy path, a boundary, a failure, and the telemetry that proves it in production — because a rule you cannot observe after release is a rule you only believe in.

E1 · Eligibility window and condition
Rule
An unopened item bought by the signed-in customer and delivered 30 days ago or less is accepted automatically. The window is evaluated on the delivery date in the store's timezone, not on the submission timestamp.
Happy
Given an unopened domestic item delivered 12 days ago and a signed-in purchaser, when they submit a return, then the decision is accepted, exactly one label intent is recorded in the same transaction, and the decision is visible to the customer within 800 ms at p95.
Boundary
Given the same item delivered 31 days ago, then the decision is refused with reason code outside_return_window, the refusal states the delivery date it used, and an appeal path is offered.
Boundary
Given delivery exactly 30 days ago at 23:50 store time and submission at 00:10 store time the following day, then the decision is accepted: 30 days is inclusive and measured in whole days from the delivery date. This example exists because the first implementation used UTC timestamps and refused it.
Guardrail
A keyboard-only customer can complete the happy path; every refusal reason is announced by a screen reader and never conveyed by colour alone.
Telemetry
eligibility_decision_total{outcome, reason_code}, and eligibility_decision_seconds as a histogram. A shift in the reason-code mix is the earliest signal that a policy change did something unintended.
Traces to
Outcome (latency half) · driver D2 · assumption A1.
E2 · Fraud hold
Rule
A customer with three or more returns marked “not received” in the trailing 90 days is referred, never auto-refused, and no label is created until an agent releases the hold. The customer is never shown the fraud reason.
Happy (from the guardrail's point of view)
Given a customer with three such returns in the last 90 days, when they submit an eligible return, then the decision is referred with internal reason fraud_review, the customer sees “a specialist is checking this — you'll hear within 4 hours”, and no label intent is recorded.
Boundary
Given a customer whose third such return was 91 days ago, then no hold is applied and the normal decision runs.
Failure
Given a held return that is still unanswered after 4 hours, then an alert routes to Nils and the ageing hold appears on the operations dashboard. An unanswered hold is a customer waiting, not a queue statistic.
Guardrail
No customer-visible text, email, or API response distinguishes a fraud hold from any other referral. A negative test asserts that fraud_review never appears in any customer-facing payload.
Telemetry
fraud_hold_rate by cohort and by exposure stage; fraud_hold_age_seconds max. Both feed the rollout guardrail, not just a dashboard.
Traces to
Guardrail “fraud loss ≤ 2.4/1,000” · risk R1 · assumption A6 · rollout stop threshold.
E3 · Carrier label failure
Rule
The customer's return is accepted independently of the carrier. Exactly one carrier label exists per accepted return, however many times we retry.
Failure
Given an accepted return whose label intent is recorded, when the carrier call times out after 5 seconds, then the return stays in awaiting_parcel with label_state = pending, the customer sees “your label is being prepared”, and the worker retries with the same client reference.
Failure
Given that retry, when the carrier had in fact created a label on the first attempt, then the original label is returned and no second label is created. Asserted directly: exactly one carrier_label_id per return, checked as a database constraint and as a daily reconciliation against the carrier's own list.
Failure
Given the carrier is unavailable for 30 minutes at peak, then eligible requests are still accepted, and 99% of pending labels are created within 15 minutes of recovery.
Operability
Given label failures exceed 2% for five minutes, then an alert fires with the carrier's error breakdown, and on-call can disable label attempts within ten minutes. Disabling label attempts must not refuse new returns, and the worker must stop retrying the existing backlog — see the reversal rehearsal, which is where we discovered it did not.
Telemetry
carrier_call_seconds, carrier_error_total{code}, label_pending_age_seconds max, duplicate_label_total (must stay at zero).
Traces to
Drivers D1 and D5 · assumption A3 · ADR 0003 · rollout stop threshold.
E4 · Refund reversal and compensation
Rule
The returns module requests refunds; it never asserts settlement, and it never reverses one. A settled refund can only be offset by a new fact created by Payments.
Failure
Given a refund authorised on a receipt event, when the warehouse later reverses that receipt (wrong item scanned), then the return moves to settlement_disputed, no compensating charge is issued automatically, and a case is opened with Payments carrying the original correlation id. Rationale: reversing money is Payments' decision and often a human's; a returns module that could do it silently would be a fraud vector.
Boundary
Given an accepted return whose parcel has not arrived within 21 days, then the return moves to expired with reason parcel_not_received, the customer is notified exactly once, and no refund is requested.
Failure
Given a refund request that Payments rejects because the captured amount is lower than the requested amount, then the return moves to settlement_disputed and is queued for an agent. The returns module does not attempt a smaller refund on its own: the invariant “refund ≤ captured” belongs to Payments and is enforced there.
Telemetry
settlement_disputed_total{cause}; the age of any accepted return with no terminal state (feeds the I4 reconciliation sweep).
Traces to
Invariants I3 and I4 · the “settlement” vocabulary entry · driver D3 (nothing crosses a trust boundary silently).
E5 · Duplicate submission
Rule
One intent creates at most one return. A repeated intent is the same return; a genuinely new intent for an already-open line is refused with a link, not silently merged.
Failure
Given a customer double-taps submit, so the same Idempotency-Key arrives twice, when the second request is processed, then the same return id, decision and label are returned with 200, and exactly one return exists.
Boundary
Given the same Idempotency-Key but a different request body, then the response is 409 with code idempotency_key_reuse. Silently returning the first result would hide a client bug; silently processing the second would create two returns.
Boundary
Given a new intent key for an order line that already has an open return, then the request is refused with reason return_already_open and a link to the existing return.
Guardrail
Given a customer edits the order id in the URL to another customer's order, then the response is 404, an authorization event is logged with the actor and the attempted id, and no field of the other order appears in any response, log line or error message.
Telemetry
idempotent_replay_total, idempotency_conflict_total, authz_denied_total{reason}.
Traces to
Driver D3 · invariant I1 · the idempotency section of the contract review.

Traces to Every example above names its own trace. Collectively: outcome (E1) · guardrails (E2, E5 guardrail) · drivers D1, D2, D3, D5 (E1, E3, E5) · invariants I1–I4 (E4, E5) · rollout thresholds (E2, E3). Method: lessons 0042 and 0049.

6 · Prioritized architecture drivers

Five drivers, not fifty. Each is written as a full quality-attribute scenario — source, stimulus, environment, artifact, response, measure — because a quality without a situation and a number cannot distinguish two designs, and a driver that cannot distinguish two designs is context, not a driver.

Driver set · returns pilot · ranked by impact × uncertainty × structural reach
#QualityScenario (source / stimulus / environment / artifact / response / measure)Why it is ranked hereEvidence mechanism · owner
D1Availability The carrier becomes unavailable for 30 minutes during peak returns traffic. The returns module and label worker must continue to accept and decide eligible requests, and create 99% of the queued labels within 15 minutes of carrier recovery. Highest reach: it decides whether the carrier call sits inside the request. Uncertainty was highest too, until the A3 spike measured it. Recovery drill in staging with the carrier sandbox forced to 100% timeouts, run before every exposure stage · Dana
D2Performance Customers submit returns at 40 requests/second at seasonal peak. The eligibility decision path must return a decision within 800 ms at p95, excluding label creation. Directly carries the outcome's latency half, and it is the driver that forces label creation off the request path. Conflicts with D1 — see below. Load test at 40 rps against a production-shaped dataset in the pipeline; production histogram with a p95 alert · Sam
D3Security & privacy A signed-in customer substitutes another customer's order identifier, in any environment including staging. The returns API must disclose no field of the other customer's order or return, in responses, errors, or logs, and record the attempt. Impact is unbounded and non-negotiable; reach is moderate but it constrains every read path and every log line. Negative authorization tests in the contract suite; a log-scanning fitness function that fails the build on an unredacted order id · Ola
D4Modifiability The policy owner adds one eligibility rule, mid-quarter, nine times a year. One engineer must release it within one working day, changing no carrier adapter, no UI component, and no API contract. Evidenced by A7 (9 changes/yr, 4 mid-quarter). It is the reason eligibility is a module with its own seam rather than rules scattered through the journey. Import-rule fitness function in the build (below); measured lead time for the last three policy changes · Dana
D5Operability The carrier starts failing: label errors exceed 2% for five minutes during any exposure stage. On-call must identify the carrier as the cause and disable label attempts within ten minutes, without refusing new returns and without leaving the worker retrying. Lower reach than D1 but it is what makes D1's response actually reachable by a human at 02:00. Its rehearsal found a real defect. Alert with a runbook link; the disable path exercised in the reversal rehearsal before each stage · Nils

The conflicts, written down before an engineer discovers them mid-sprint. D1 and D2 pull against a synchronous carrier call: D2 wants the decision back in 800 ms, and the A3 spike measured the carrier at p99 11.4 s, so a live label inside the request cannot satisfy both. The team's resolution is to return the eligibility decision quickly and make label creation a separately observable state — which is exactly the decision recorded in ADR 0003, and which costs the customer a two-step mental model (“accepted” then “label ready”), the cost that assumption A2's comprehension test exists to measure. D3 and D5 also conflict in miniature: on-call wants richer failure payloads in logs, and D3 forbids identifiers in them. Resolution: correlation ids in logs, identifiers only behind an access-controlled lookup.

Considered and rejected as drivers · with the reason
ProposedWhy it is not a driver here
“The system must scale.”No load, operation or horizon. Once written out, the real load is 40 rps at seasonal peak (D2), which a single deployable handles. No credible design in the option table responds differently to “scale”, so it distinguishes nothing.
“Must handle 10× current volume.”10× is beyond the twelve-month horizon in the demand forecast, and the pilot is 1% of one cohort. Designing for it now would buy an architecture we cannot test and would not be able to justify in ADR 0001.
“Must be cloud-native / event-driven.”These are mechanisms, not qualities. They belong in the option table as candidate answers, and both appear there — where they can be compared and rejected with a reason.
“Must be maintainable.”Replaced by D4, which names the change (one policy rule), the person (one engineer), the elapsed time (one day) and the blast radius (no adapter or contract change). That version can fail a check; “maintainable” cannot.
“99.99% uptime.”A number without an operation. The pilot's real availability requirement is D1: accepting requests must survive a carrier outage. Label creation may degrade; request acceptance may not. One number for the whole system would have hidden that distinction.

Traces to Outcome (D2 carries the latency half) · guardrails (D3 privacy, D5 the queue's operability) · assumptions A3 (D1), A7 (D4), A6 (D5's disable path) · risk R1 and R3. D1 and D2's conflict is the direct input to ADR 0003; D4 is the direct input to the module seam in the container view. Method: lessons 0043 and 0059.

7 · Architecture views

Four views, each with a stated audience and a question it answers. Anything that helps neither is deleted. The rule the team enforced in review: every comment must point at a driver, a workflow step, or a risk — otherwise it is decoration and gets no diagram time.

7.1 System context

Context view · v3 · 12 September · audience: product, security, platform · question: what is ours, and where does uncontrolled behaviour enter?
ElementKindRelationship to the returns systemProtocolTrust boundaryPersonal data crossing
CustomerPersonSubmits a return, reads status, appeals a refusalHTTPS, session-authenticatedPublic internetName, address, order history (theirs only)
Support agentPersonDecides referred returns, releases fraud holds, overridesHTTPS, staff SSO + roleCorporateFull return detail, audited on read
Returns systemSystem in scopeDecides eligibility, records intent, tracks the journey, requests refunds——Retains name, address, reason for 30 days
Order platformExternal, internal teamAuthoritative for order, delivery date, captured amount. Read-only to us.Internal HTTP, synchronousInternal, same estateWe read; we do not copy order history
CarrierExternal, third partyCreates labels; we send the minimum needed to print oneHTTPS + API key, outbound onlyVendorRecipient name and address only — never the return reason
PaymentsExternal, internal teamAuthoritative for settled money. We request; it decides.Internal HTTP + async settlement eventInternal, PCI scopeNo card data enters returns (constraint)
Warehouse systemExternal, internal teamPublishes receipt facts; a clerk confirms them manually during the pilotEvent stream, at-least-onceInternalOrder id and item only
  CONTEXT · returns resolution (v3, 12 Sep)
  legend:  [ ] person   ( ) system in scope   { } external system   ==> trust boundary

     [Customer]                       [Support agent]
         |  submit / track (HTTPS)         |  decide referrals (SSO)
         v                                 v
     +---------------------------------------------+
     |          ( Returns system )                  |
     |  decides eligibility, records label intent,  |
     |  tracks the journey, requests refunds        |
     +---------------------------------------------+
        |  read order        |  create      |  request        ^ receipt
        |  (sync)            |  label       |  refund         | events
        v                    v ==vendor==>  v                 |
   {Order platform}     {Carrier}      {Payments}     {Warehouse system}
    authoritative:       one label      authoritative:   authoritative:
    order truth          per intent     settled money    parcel received
  

The three “authoritative:” labels are the most load-bearing text on the diagram. They say that the returns system holds no independent opinion about order truth, settled money, or whether a parcel arrived — and that is what makes the state machine in section 8 enforceable rather than aspirational.

7.2 Containers

Container view · v3 · 12 September · audience: the team and platform · question: what runs, what stores data, and which driver pays for each boundary?
ContainerTechnologyResponsibilityData it ownsOwnerDriver that justifies it existing separately
Existing web appServer-rendered pages in the existing storefrontCustomer-facing return flow and status pageNoneMarco / SamNone needed — it already exists; adding a new frontend would be a boundary with no driver
Returns modulePython package inside the existing storefront deployableEligibility decisions, return journey state, API, notificationsreturn, eligibility_decision, label_intent, return_eventDanaNone — deliberately not a separate deployable. See ADR 0001.
Label workerSame artifact, separate process, same databaseDrains label intents, calls the carrier idempotently, retries with backoffNone — it reads and updates label_intentLeaD1: the carrier call must survive a 30-minute outage without blocking request acceptance. A separate process, not a separate service.
Existing databaseThe storefront's PostgreSQL, new schema returnsDurable state and the outbox tableThe four tables above; no other module may write themDanaNone — a second store would add operational surface with no driver. Ownership is enforced by schema grants, not by convention.
Reconciliation jobSame artifact, scheduled dailySweeps for I4 breaches and for carrier labels we do not know aboutNoneLeaD5: the reconciliation condition needs a detector with an owner, or it is a wish.
  CONTAINERS · one deployable, two processes, one schema (v3, 12 Sep)
  legend:  [==] process    (--) datastore    -->  in-process call    ~~>  async

   +----------------------- one deployable artifact -----------------------+
   |                                                                       |
   |  [== web process ==]                      [== label worker ==]        |
   |   storefront pages                         drains label_intent        |
   |        |                                        |      ^              |
   |        v                                        |      | retry w/     |
   |   +--------------------------+                  |      | same ref     |
   |   |  returns module          |                  v      |              |
   |   |  +--------------------+  |            ==vendor boundary==>        |
   |   |  | eligibility        |  |                {Carrier}               |
   |   |  |  (no I/O, no       |  |                                        |
   |   |  |   vendor types)    |  |            [== reconciliation job ==]  |
   |   |  +--------------------+  |             daily I4 + label sweep     |
   |   |  | return journey     |  |                                        |
   |   |  +--------------------+  |                                        |
   |   |  | adapters: order,   |  |                                        |
   |   |  |  carrier, payments |  |                                        |
   |   |  +--------------------+  |                                        |
   |   +--------------------------+                                        |
   |          |          ^                                                 |
   |          v          |                                                 |
   |      (-- returns schema --)  outbox: label_intent, return_event       |
   +-----------------------------------------------------------------------+
              |                    |                     ^
              v                    v                     | at-least-once
        {Order platform}      {Payments}          {Warehouse events}
  

The container view's job here is mostly to say what is not there. There is no returns service, no message broker, and no second database. Each absence is an accepted cost recorded in ADR 0001: the team gives up independent scaling and independent release of eligibility, and gains one deployment, one transaction, and no distributed failure modes for a seven-person team with a fixed pilot date. The label worker is a separate process — the smallest boundary that satisfies D1 — precisely so that no network hop is introduced where a transaction will do.

7.3 Module and dependency model

Inside the deployable, the boundaries come straight from the workflow evidence: eligibility owns policy interpretation, the return journey owns customer-visible progress, and adapters own vendor vocabulary. The dependency direction is one-way and enforced in the build rather than in a README, which is what turns D4 from a hope into a checkable property.

Allowed dependencies · enforced by a build check
ModuleMay importMay not importWhy
eligibilitynothing in the module treereturn_journey, any adapter, any vendor SDKD4: a policy change must not require touching an adapter. A pure decision function is also the cheapest thing to test at the 200-case scale A1 produced.
return_journeyeligibility, adapter portsadapter implementations, vendor SDKsDepends on the vocabulary, not the vendor. Lesson 0023's rule applied.
adaptersports, vendor SDKseligibilityTranslation lives at the edge, one direction only.
apireturn_journey, portseligibility directlyOne entry point per capability; the API cannot bypass the journey's guards.
ALLOWED = {
    "eligibility": set(),
    "return_journey": {"eligibility", "ports"},
    "adapters": {"ports"},
    "api": {"return_journey", "ports"},
    "ports": set(),
}


def import_violations(graph: dict[str, set[str]]) -> list[str]:
    """Fitness function for driver D4: the boundary map, enforced in the build."""
    violations = []
    for module, imports in sorted(graph.items()):
        for target in sorted(imports - ALLOWED[module]):
            violations.append(f"{module} must not import {target}")
    return violations


assert import_violations({"eligibility": {"adapters"}}) == [
    "eligibility must not import adapters"
]
assert import_violations({"return_journey": {"eligibility"}}) == []

7.4 Dynamic view · happy path

Dynamic view A · v3 · audience: the team · question: where can work, identity or truth be lost?
#InteractionSync / asyncFailure semantics
1Customer submits the return with an Idempotency-KeysyncA replay returns the same return (E5)
2API reads order, delivery date and captured amount from the order platformsync, 200 ms budgetTimeout ⇒ 503 with retry_after; nothing is written. The customer's request has not been accepted, and we say so.
3return_journey calls eligibility.assess(request)in-processCannot fail on I/O — it performs none. That is the point of the seam.
4One transaction: insert return, insert eligibility_decision, insert label_intent, insert return_event (outbox)syncAll four commit or none do. This is the consistency boundary — the reason no message broker appears in the container view.
5Response to the customer: accepted, label pendingsyncp95 800 ms (D2) — measured from 1 to 5, excluding everything after
6Label worker claims the intent and calls the carrier with the intent id as the client referenceasyncAt-least-once; the client reference makes the carrier call idempotent (E3)
7label_intent updated with carrier_label_id; LabelAttached appendedasyncUnique constraint on (return_id) makes a second label impossible to persist even if one is created
8Notification sent to the customerasyncSent from the event, after commit — never inside the transaction (lesson 0025)
9Warehouse receipt event consumed; refund requested from PaymentsasyncDeduplicated on the receipt event id; the refund request carries the return id as its idempotency key
10Payments emits settlement; the return moves to refundedasyncOnly this event may move the return to refunded (I5)

7.5 Dynamic view · carrier label timeout

Dynamic view B · v3 · audience: the team and on-call · question: what does the customer see, and what does on-call do?
#InteractionState afterCustomer seesOperator sees
1Steps 1–5 of the happy path complete normallyawaiting_parcel, label_state=pending“Accepted. Your label is being prepared.”—
2Worker calls the carrier; the call times out at 5 sunchangedunchangedcarrier_call_seconds spikes
3Worker retries at 2 s, 8 s, 32 s, then every 2 min, always with the same client referenceunchangedunchangedlabel_pending_age_seconds rises
4Carrier error rate passes 2% for 5 minutesunchangedunchangedAlert with error breakdown and runbook link (D5)
5The carrier had actually created a label on attempt 1; the retry returns itlabel_state=ready, one carrier_label_idLabel available; email sentduplicate_label_total stays at 0
5′Alternative: the outage lasts 30 minutes. On-call disables label attempts.intents stay queued; new returns are still accepted and decided“Accepted. Labels are delayed — we'll email yours.”Worker stops retrying within one loop iteration (the defect the rehearsal found)
6′Carrier recovers; the flag is re-enabled99% of queued intents get labels within 15 minutes (D1)Label email arrivesBacklog drains; alert clears
7′Worst case: the outage exceeds 4 hoursReturns older than 4 h with a pending label are referred to agents, who issue labels from the carrier portal manuallyAgent contact within the SLAManual queue rises — watched against the 50/day guardrail

Row 7′ is the artifact that most often goes missing. The fallback for a long carrier outage is the old manual process, kept alive on purpose during the pilot, with its own capacity number and its own guardrail. An architecture that has no answer for “the vendor is down for four hours” has quietly promised that this never happens.

Traces to Drivers D1 (worker process, view B), D2 (steps 1–5 budget), D3 (context trust boundaries and data minimisation), D4 (module import rules), D5 (view B rows 4–6′) · constraints (order platform and Payments authoritative) · risk R3 · rollout reversal rehearsal. Method: lessons 0044, 0056, 0059.

8 · State machine and contract review

“Return status” looked like a field in the brief. It is a machine with eleven states, six actors and four rules that must never be violated. Designing it before the schema is what stops return.status = "approved" from being assignable from anywhere in the codebase.

Return aggregate · state-transition table
FromCommandToActorPrecondition / invariantAuthoritative ownerEmitted factRecovery if wrong
—SubmitReturnrequestedCustomerNo other open return for this order line (I1); order line exists and belongs to the actorReturn journeyReturnRequestedCancel (below)
requestedAssessEligibilityacceptedPolicy engineExactly one decision per return; window and condition rules pass; no fraud holdEligibilityReturnAccepted, LabelIntentRecordedReversal is legal only before a label exists
requestedAssessEligibilityrefusedPolicy engineA reason code and human text are mandatory; no refusal without bothEligibilityReturnRefusedAppeal (forward transition, not a reversal)
requestedAssessEligibilityreferredPolicy engineJudgement case or fraud hold; 4-hour agent SLA starts nowEligibilityReturnReferred—
referredAgentDecidesaccepted / refusedSupport agentAgent identity recorded; if it contradicts a policy rule, an override reason is mandatoryEligibilityReturnAccepted / ReturnRefused + PolicyOverridden—
refusedAppealunder_reviewCustomer via supportOne appeal per refusal; new evidence requiredEligibilityAppealOpened—
under_reviewUphold / Overturnrefused / acceptedPolicy ownerWho upheld or overturned, and why, is recordedEligibilityAppealDecided—
acceptedAttachLabelawaiting_parcelLabel workerExactly one carrier label per return (I2) — a unique constraint, not a code checkReturn journeyLabelAttachedVoid the label with the carrier; the return returns to accepted
awaiting_parcelRecordReceiptreceivedWarehouse clerk (manual, pilot)The scanned item must match the return; receipt event id deduplicatedWarehouse (external)ParcelReceivedReceipt reversal ⇒ settlement_disputed (E4)
awaiting_parcelExpireexpiredReconciliation job (time)21 days since acceptance with no receipt; customer notified exactly onceReturn journeyReturnExpiredAn agent may re-open within 7 days
receivedRequestRefundrefund_requestedReturn journeyNever sets settlement; carries the return id as its idempotency keyReturn journeyRefundRequestedRetry is safe by construction
refund_requested—refundedPayments onlyI5: no other actor may write this state. Refund ≤ captured amount is enforced by Payments, not by us (I3)Payments (external)RefundSettled (consumed)None. Money has moved; only a new Payments fact can offset it
refund_requested / refundedDisputeSettlementsettlement_disputedReconciliation job or agentReceipt reversed, or Payments rejected the requestReturn journeySettlementDisputedCompensation, decided by a human in Payments
requested / acceptedCancelcancelledCustomerOnly before a parcel is received; any attached label is voided with the carrierReturn journeyReturnCancelledSubmit a new return

Two rows in that table carry most of the design. Only Payments may write refunded: the returns module requests and observes, so the invariant “a refund never exceeds the captured amount” lives where the captured amount lives, and our module physically cannot break it. And the refund row has no recovery, which is written down rather than hidden behind the word “rollback”. A code deploy can be reverted; a settled refund cannot. That single admission is what produced the settlement_disputed state and E4's rule that we never auto-compensate.

Rules, classified — because not everything called an invariant is one
IdRuleKindWhere it is enforcedWhat happens when it fails
I1An order line has at most one open return.Hard invariantPartial unique index on (order_line_id) where state is not terminal, plus a domain guardRefuse with return_already_open and a link (E5)
I2A return in awaiting_parcel has exactly one carrier label.Hard invariantUnique constraint on label_intent.return_id; the carrier call is idempotent on the intent idRefuse the second write; alert; daily reconciliation against the carrier's own label list
I3A refund never exceeds the captured amount.Hard invariant owned by another systemPayments. We cannot enforce it and do not pretend to.Payments rejects; we move to settlement_disputed (E4)
I4Every accepted return reaches a terminal state within 21 days.Reconciliation condition, not an invariant — it is temporarily false by designDaily sweep; detection within 24 hours; owner Nils; escalation to Dana after two consecutive daysReferred to an agent with the ageing reason; counted against the manual-queue guardrail
I5Only Payments may assert settlement.Ownership ruleNo code path in the returns module writes refunded; a fitness function greps for one and fails the buildBuild failure, before it can be a production defect
—Only the assignee or an agent with the returns role may decide a referral.Authorization precondition (not an invariant)API edge plus a domain guard, deliberately separate so error codes tell the caller which edge they hit403 with not_permitted; logged with actor

8.1 Internal contracts

The seam between eligibility and the return journey is an in-process contract, and it is the one the modifiability driver depends on. It speaks the domain's language, performs no I/O, and returns a value that is complete enough that the caller never has to ask a follow-up question — the definition of a deep module.

Internal contract · eligibility → return journey
Command
assess(request: ReturnRequest) -> EligibilityDecision
Promise on success
Exactly one decision, complete: outcome (accepted / refused / referred), a reason code and human text whenever the outcome is not accepted, the policy_version used, the timestamp, and the deciding actor. Deterministic given the same request and policy version.
Forbidden
No network or database I/O. No vendor types in the signature or the result. No knowledge of how the decision will be displayed or stored. Enforced by the import rule in 7.3.
Failure
There is no failure mode. Missing input is a programming error and raises; there is nothing to retry, because there is nothing to fail. This is the payoff of keeping I/O out.
Events emitted after commit
ReturnAccepted, ReturnRefused, ReturnReferred — each carrying return id, decision, reason code, policy version, correlation id. Published from the outbox, never inside the transaction.
Proof
The 200 sampled agent decisions from A1 run as a table-driven test suite against assess. A policy change that alters an existing sampled outcome fails the build and must be signed off by Anya.
from dataclasses import dataclass
from datetime import date, timedelta
from enum import Enum


class Outcome(Enum):
    ACCEPTED = "accepted"
    REFUSED = "refused"
    REFERRED = "referred"


@dataclass(frozen=True)
class EligibilityDecision:
    outcome: Outcome
    policy_version: str
    reason_code: str | None = None
    reason_text: str | None = None

    def __post_init__(self) -> None:
        # A refusal or referral without a reason is not a decision the
        # customer, the agent, or the telemetry can act on.
        if self.outcome is not Outcome.ACCEPTED and not self.reason_code:
            raise ValueError("a non-acceptance must carry a reason code")
        if self.reason_code and not self.reason_text:
            raise ValueError("every reason code ships with human text")


def assess(request, policy, holds) -> EligibilityDecision:
    """No I/O: policy and holds are already-loaded values, not clients."""
    if holds.fraud_hold_for(request.customer_id):
        return EligibilityDecision(
            Outcome.REFERRED, policy.version,
            "fraud_review", "A specialist is checking this.")
    age = request.submitted_on - request.delivered_on
    if age > timedelta(days=policy.window_days):
        return EligibilityDecision(
            Outcome.REFUSED, policy.version,
            "outside_return_window",
            f"Delivered on {request.delivered_on}, outside the "
            f"{policy.window_days}-day window.")
    if request.item_condition != "unopened":
        return EligibilityDecision(
            Outcome.REFERRED, policy.version,
            "condition_needs_review", "An agent will review this item.")
    return EligibilityDecision(Outcome.ACCEPTED, policy.version)

8.2 External contract

External contract · returns API · v1
QuestionDesigned answer
What is the resource?A return. The eligibility decision is a child resource of it, not a mutable field on it.
What names identify it?POST /v1/returns to create; GET /v1/returns/{id}; POST /v1/returns/{id}:appeal as a custom method that reads like the domain verb.
What does success promise?201 means: the return exists durably, a decision has been made, and if accepted, exactly one label intent is recorded. It does not promise a label exists — that is label_state, and the distinction is the whole point of ADR 0003.
How are retries deduplicated?A mandatory Idempotency-Key header on every unsafe method. See the key table below.
What does a failure mean?Every error carries code (a stable string), message (human text), and retryable (boolean). 409 means “your intent conflicts with a stored one — do not retry blindly”. 503 carries retry_after. A timeout means unknown: the caller must retry with the same key, never submit afresh.
Which edge rejected me?400 = schema; 403 = authorization; 422 = a valid request the domain refuses. Collapsing these into 400 is what makes consumers guess.
What data crosses?Request: order line id, item condition, reason. Response: decision, reason text, label state, label URL when ready. The carrier receives recipient name and address only — never the return reason (D3, data minimisation).
Compatibility rules · written down, because “be tolerant” is not a policy
ChangeBreaking?Rule
Adding an optional response fieldNoFree. Consumers must ignore unknown fields; asserted by a consumer contract test that sends an unknown field and expects success.
Adding a new reason_code valueNo, by constructionreason_code is an open enum. Every code ships with reason_text, and consumers must render the text for codes they do not recognise. This is why the internal contract refuses to construct a code without text.
Adding a new value to stateYesstate is a closed enum: consumers branch on it. A new state requires a version bump and a migration window. referred was added in v1 before any external consumer existed — after that, it would not have been free.
Making an optional request field requiredYesNew version. Never quietly enforced.
Changing what an existing field meansYes — and the most dangerous kindNew field, new name, expand/contract. Redefining label_state from “we have a label” to “we have asked for one” would pass every schema check and break every consumer.
Removing a fieldYesExpand, migrate readers, verify with consumer contract tests, then contract. Minimum 90-day deprecation window; migration owner named in the deprecation notice.
Idempotency keys · one row per unsafe operation
OperationKeyScopeTTLSame key, different body
POST /v1/returnsClient-supplied Idempotency-Key; the UI derives it from the order line id plus the form sessionPer customer24 hours409 idempotency_key_reuse (E5)
Carrier label creationlabel_intent.id sent as the carrier's client referencePer carrier accountCarrier-defined, 7 daysImpossible — the worker never changes an intent
Refund request to Paymentsreturn_idPer merchantPermanentPayments rejects; we move to settlement_disputed
Warehouse receipt consumptionreceipt_event_id, stored in a processed-events tablePer stream90 daysSecond delivery is a no-op

Traces to Constraints (order platform and Payments authoritative; no card data) · driver D3 (edges, data minimisation) and D4 (the internal seam) · assumptions A3 and A4 (idempotency and receipt dedup) · risk R1 (fraud hold as a decision, not a hidden refusal) · examples E3, E4, E5 · the workflow's “approved means three things” hotspot. Method: lessons 0045, 0054, 0055, 0057.

9 · Options, spike, and ADR 0003

The team's provisional design in week 1 called the carrier inside the request: it was the least code and gave the customer a label immediately. The A3 spike killed it. What follows is the comparison as it was actually written — three genuinely different mechanisms, not one mechanism with three vendors — and the record that replaced the earlier decision.

Option table · what to do when the carrier is slow or down
Option A · synchronous callOption B · persist intent, drain from a workerOption C · accept, then have an agent buy the label
MechanismThe request calls the carrier and returns the label with the decision.The request commits the return, the decision and a label intent in one transaction. A worker drains intents and calls the carrier idempotently.The request commits the return and the decision. The label appears in the agent queue; an agent buys it in the carrier portal, as today.
Drivers satisfiedSimplest operationally; one state fewer for the customer to understand (helps A2).D1 (accepts during an outage), D2 (decision path excludes the vendor), D5 (the vendor is separately observable and separately disableable).D1 and D2 as well; the smallest possible amount of new code.
Drivers harmedD1 and D2 both fail: at carrier p99 = 11.4 s the 800 ms budget is impossible, and a 30-minute outage refuses every request.A2 is harmed: the customer now sees “accepted, label coming” rather than a label. A new process is on-call surface (D5 cost).Guardrail: one manual label purchase per accepted return — about 10/day at 10% exposure, about 96/day at full exposure — on top of the 34/day of referrals the queue already carries at full exposure. The 50/day manual-queue guardrail breaks somewhere between the 10% and 50% stages.
Delivery costLowest. About 2 days.Worker, outbox table, retry policy, backlog telemetry, disable flag. About 6 days.Near zero engineering; an agent runbook.
Operational costNothing new, but every carrier incident becomes a customer-facing outage.A queue to watch, a backlog to age out, a flag to own and delete.Permanent human cost that grows linearly with success — the worst shape of cost for a pilot that intends to scale.
ReversibilityHigh — but reversing it after launch means changing what the API's 201 promises, which is a contract change.Medium: the pending state becomes visible to customers and to any consumer, so removing it later is a contract change too.High — it is the current process; turning it off costs nothing.
Largest unknownResolved: carrier p95/p99 and duplicate behaviour (A3).Whether customers understand a two-step flow (A2 — still inconclusive at n = 5).Whether the manual queue survives the exposure ramp. The volume model says it does not.
VerdictRejected on measured evidence.Chosen.Rejected as the primary mechanism; retained as the fallback for a carrier outage longer than four hours (dynamic view B, row 7′).

Option C deserves a moment. It was not a straw man, and it was not fully discarded: it lost as the everyday mechanism because its cost scales with success, and it won as the emergency mechanism because it needs no new code and no vendor. Keeping a rejected option alive in a narrower role is often the most useful outcome of an option table.

Spike record · carrier label latency and duplicate behaviour
Decision blocked
Whether label creation may sit inside the request that accepts a return.
Question (falsifiable)
At the pilot's request rate, is the carrier's label-creation p95 under 500 ms, and does a retry after a timeout create a second label?
Timebox
Four hours, one engineer (Dana). Publish numbers whatever they are.
Method
Carrier sandbox: 200 sequential creates recording p50/p95/p99; then 20 creates with the client socket timeout forced to 1 s, each retried once — ten with a client reference, ten without.
Result
p50 640 ms, p95 1.9 s, p99 11.4 s. With a client reference: 0 duplicates in 10. Without: 2 duplicates in 10. Answer to the question: no, and yes.
Disposal
Harness deleted. The one thing kept was the assertion “exactly one carrier label per intent”, which became a production reconciliation check rather than a script on a laptop.
Caveat recorded
The sandbox is not production. The p99 figure is directionally trusted, not contractually. This is why D1's evidence mechanism is a recurring drill, not a one-off number.
ADR 0003 · Persist label intent before calling the carrier
Status
Accepted · 12 September · decider: Dana (tech lead) · advice from Priya, Marco, Nils, Lea, Sam
Supersedes
ADR 0002 (“Create the carrier label inside the request”, accepted 1 September). ADR 0002 remains in the log, unedited, marked superseded — it is a historical fact about what the team believed on 1 September.
Superseded by
None. See “Revisit when”.
Context
The pilot must accept eligible returns and communicate a decision within 4 hours median, at up to 40 rps at seasonal peak, with a p95 decision path of 800 ms. The carrier is a fixed contractual dependency. The A3 spike measured the carrier at p50 640 ms, p95 1.9 s, p99 11.4 s, and observed duplicate labels on 2 of 10 timeout-retries made without a client reference. The team is seven people with one deployable and a fixed 30 September date.
Forces
D1 availability (accept during a 30-minute carrier outage; 99% of labels within 15 minutes of recovery). D2 performance (800 ms p95 decision, excluding label). D5 operability (disable label attempts within 10 minutes). Guardrail: manual review queue ≤ 50/day. Assumption A2 (customer comprehension of a two-step flow) is not yet settled and is the main cost of this decision. Operational simplicity for a seven-person team.
Options considered
A. Synchronous carrier call — rejected: measured p99 exceeds the decision budget by 14×, and a carrier outage would refuse eligible requests, failing D1 outright. C. Agent buys the label — rejected as the everyday mechanism: it adds ~40 manual actions/day at pilot volume and breaks the 50/day guardrail at roughly 30% exposure; retained as the outage fallback. B. Persist intent + worker — chosen.
Decision
The API commits the return, the eligibility decision and a label_intent row in a single transaction, and returns a decision with label_state = pending. A separate worker process in the same deployable drains intents and calls the carrier, passing the intent id as the carrier's client reference so retries are idempotent. A unique constraint on label_intent.return_id makes a second label unstorable. Label attempts sit behind a flag that on-call can disable in under ten minutes, checked inside the worker loop as well as at request time.
Consequences · benefits
Request acceptance survives a carrier outage. The 800 ms budget is achievable and measurable, because the vendor is no longer inside it. Carrier health becomes separately observable and separately disableable, which is what makes D5 reachable.
Consequences · accepted costs
(1) The customer sees two steps — “accepted”, then “label ready” — and A2 says two of five test participants misread the first as a refusal. We accept this cost and pay down the risk with wording work and a retest, not by reversing the decision. (2) A worker process, a backlog, and an ageing metric now exist and must be watched; that is new on-call surface for a team of seven. (3) A pending state is now visible in the API, so removing it later is a contract change, not a refactor. (4) The label flag is temporary machinery with a deletion owner and date (see closeout).
Consequences · follow-up work
Reversal rehearsal of the disable path before each exposure stage (Nils). Daily reconciliation against the carrier's label list (Lea). A2 retest at n = 12 (Marco, 15 September).
Operational ownership
Worker health, backlog age and the disable flag belong to on-call from the staff-accounts stage onward; transferred to the steady-state owner at closeout.
Dissent
Recorded, from Sam (engineer). “For a pilot of about 50 requests a day we are buying a queue, a backlog metric, a flag and an on-call surface to solve a problem that option C solves with a runbook. We should ship C and revisit at 10% exposure.” The concern is legitimate and was not resolved by argument. What would prove it right: if, by the 10% stage, the label worker has produced no incident that the manual path would also have produced, and the manual queue has stayed under 20/day, then the worker was premature and we should say so in the closeout. Outcome at closeout: referrals alone reached 41/day at peak, so option C's extra label purchases would have breached the 50/day guardrail — the dissent was recorded, tested, and not upheld. Sam's second point, that the flag is carrying cost, was upheld: it is on the removal list with a date.
Revisit when
Any one of: sustained volume exceeds the worker's measured drain rate of 12 labels/minute; the carrier publishes a durable asynchronous label API; the label queue misses its 15-minute recovery target twice in one quarter; or the A2 retest shows the two-step flow costs more contacts than the outage risk it avoids.
Decision log · the whole set, so ADR 0003 has a context
ADRDecisionStatusAccepted cost
0001Build the returns capability as modules inside the existing storefront deployable, not as a service.Accepted, 28 AugNo independent scaling or release of eligibility. Revisit when a driver — not a preference — requires one.
0002Create the carrier label inside the request that accepts the return.Superseded by 0003, 12 Sep—
0003Persist label intent; drain from a worker; idempotent on the intent id.Accepted, 12 SepAbove, in full.
0004Referral is a first-class decision outcome, not a failure to decide.Accepted, 12 SepA third state in the API's closed state enum, and a 4-hour human SLA the team must staff.
0005Warehouse receipt stays a manual confirmation for the whole pilot.Accepted, 15 SepAbout 40 clerk actions/day; removal trigger stated in the target workflow.

Traces to Drivers D1, D2, D5 (the forces) · guardrail “manual queue ≤ 50/day” (why C lost) · assumptions A3 (the evidence that changed it) and A2 (the accepted cost) · risk R3 (closed by this decision) · the container view's worker process · rollout stage entry conditions. Method: lessons 0046 and 0059.

10 · Walking skeleton and delivery map

The plan is ordered by what it retires, not by what it builds. The first thing the team produced was not a feature: it was one request travelling the entire real path with almost no behaviour in it, so that every integration surprise arrived in week 2 instead of week 9.

Walking skeleton · one sentence, and everything that is fake in it
The sentence
From the main branch, deploy one internal-user return request through the production pipeline; persist its intent, cross the carrier port with a fake adapter, render pending label on the real status page, and find the complete request by correlation id in the real logging stack.
Real
The build, the pipeline, the deployable, the database and its migration, the module boundaries and their import check, the outbox, the worker process, the correlation id, the telemetry pipeline, the status page.
Fake or hard-coded — stated, not implied
Eligibility always returns accepted (no policy at all). The carrier adapter returns a fixed label id after 100 ms. Order lookup returns one fixture order. No notifications. No authorization beyond “is signed in”. Warehouse receipt is a button in an internal page.
What it proved in week 2
Three things nobody had predicted: the storefront's session cookie was not available to the worker process (so correlation ids broke across the boundary), the migration needed a two-step deploy to avoid locking a hot table, and the platform's log retention was 3 days, not 30 — which the privacy review had assumed. All three were cheap in week 2 and would have been expensive in week 9.
Delivery map · evidence milestones, never “percent complete”
MilestoneEvidence producedExit conditionRetiresOwner · target
Path walksOne request deploys, persists, crosses the port, and is traceable end to end from a clean buildRepeatable from a clean checkout; a forced failure is visible in the logging stack within one minuteIntegration and deployment riskDana · week 2
Policy worksThe 200 sampled agent decisions run as tests against assessAnya accepts the decisions and the refusal reasons on the sample; disagreements are logged as rules, not bugsAssumption A1Sam · week 4
Team uses itInternal users complete the accessible happy path and both refusal paths on their own devicesNo severity-one usability, accessibility or authorization defect openAssumption A2 (partly), driver D3Marco · week 5
Pilot learns1% of eligible demand exposed against a matched control; outcome and guardrail measures readableThe pre-agreed continue / change / stop thresholds can actually be evaluated at n ≥ 120Assumption A6, risk R1, the outcome itselfPriya · week 6 (29 Sep)
Scale earnsLoad at 40 rps, carrier recovery drill, support runbook exercised, steady-state owner namedEvery rollout gate passes; the disable path has been exercised twiceDrivers D1, D2, D5; operational ownershipNils · week 9
Dependency map · “waiting on the platform team” is not a plan
DependencyExternal ownerExact deliverableNeeded byReadiness evidenceFallbackEscalation trigger → who
Carrier sandbox credentials and rate limitAsha (platform)Sandbox key with 20 rps, plus the production key held in the vaultWeek 2A test call succeeded on 26 AugFake adapter behind the real port — the skeleton is designed to proceed without itNot delivered by Wednesday → platform lead
Payments refund API scopeRavi (payments)Written confirmation that partial refunds are out of scope for the pilot and that return_id is accepted as the idempotency keyWeek 3Confirmed in writing, 8 SepRefund requests queue for an agent; the pilot still measures the decision outcomeNo answer by end of week 3 → engineering director
Warehouse receipt event accessWarehouse platformRead access to the receipt stream plus 30 days of history for the A4 replayWeek 2Replay completed 3 SepManual confirmation (which the pilot uses anyway, per ADR 0005)Access refused → escalate as a scope trade, not a delay
Privacy and security reviewOlaSign-off on data minimisation to the carrier, 30-day retention, and the log-redaction fitness functionBefore staff-accounts stageDraft review 5 Sep; log retention finding raised by the skeletonNone. This is a gate, not a risk. Exposure does not start without it.Not signed 5 days before the stage → CISO
Support staffing for referralsNilsTwo agents rostered for the 4-hour referral SLA during exposure hoursWeek 6Roster published 22 SepReduce exposure percentage rather than break the SLARoster gap → head of support, same day
Forecast · a range, and the variable that moves
Fixed
The 30 September pilot-open date (seasonal). The team of seven. The outcome and its guardrails.
Movable
Scope. With a fixed date and a fixed team, scope is the only control surface, and saying so in week 1 is what makes the week-6 trade a decision rather than a crisis.
Forecast
Pilot open to 1% between 26 September and 3 October, ~70% confidence, based on four completed slices at a measured 4–7 days each. Not “30 September”: a single date at this range is a claim the evidence does not support.
Smallest scope trade that preserves the outcome
Drop the in-product appeal flow to slice 2 and keep “refused, with a reason and a link to contact an agent”. This costs a worse experience for the 11% of requests that are refused and saves about 5 days. It does not touch either outcome measure, which is the test a scope trade has to pass.
What would not be traded
The reversal rehearsal, the authorization negative tests, and the matched control cohort. Trading those buys days by removing the ability to know whether the pilot worked.
Next reforecast
At the “Team uses it” milestone, or immediately if any dependency's escalation trigger fires — not only when the date becomes impossible.

Traces to Every milestone names the assumption, driver or risk it retires. Outcome (the “Pilot learns” milestone is the first point at which the outcome is measurable) · assumptions A1, A2, A6 · drivers D1, D2, D3, D5 · risk R1 · rollout stage entry conditions. Method: lessons 0047 and 0060.

11 · Team charter and decision rights

Seven people, one outcome, and a fixed date. The charter took 45 minutes to write and settled the arguments that otherwise get re-run every week under time pressure. Its most valuable section is not the cadence — it is the column headed “must seek advice from”, which is what lets a decision be made quickly and be informed.

Team charter · returns resolution
Purpose
Move eligible domestic return resolution from 72 hours to under 4 hours and cut support contacts by a quarter, for signed-in customers with unopened items, with the pilot open by 30 September.
People and accountabilities
Priya — product (outcome, scope, cohort). Marco — product design (experience, research, accessibility). Dana — tech lead (technical integrity, architecture, risk). Sam, Lea, Tomas, Ije — engineers (implementation within agreed boundaries; Lea owns the worker, Sam owns eligibility, Tomas owns the API and adapters, Ije owns telemetry and the pipeline). Consulted, not embedded: Anya (policy), Nils (support operations), Ola (privacy and security), Ravi (payments), Asha (platform).
Missing expertise, named
Nobody on the team has run a fraud guardrail before. Consultation path: Risk, fortnightly, and Risk holds the stop authority on the fraud guardrail rather than the team.
Flow
WIP limit: two slices in progress, full stop. Swarm rule: if a slice has not moved in two days, everyone not on the other slice joins it. Review: every change reviewed, but the tech lead is not a required reviewer — guardrails are published as the import fitness function and the contract tests, so review is distributed rather than queued behind one person.
Sources of truth — one place each
Why: this frame. Scope: the story map. Current flow: the delivery board. Decisions: the ADR log. Uncertainty: the risk and dependency map. Evidence: the initiative dashboard. Nothing is duplicated into slides; a status that exists in two places will disagree within a fortnight.
Cadence
Daily 10 min (flow and blockers, not status). Twice weekly 30 min (shape the next slice with examples, product + design + engineering together). Weekly 30 min (initiative review — see section 13). Weekly 45 min (demo integrated behaviour). As needed, timeboxed (one architecture decision with the required advisers). Fortnightly 45 min (team health; one owned experiment). Any of these is cancelled for the week if it has no decision to make.
Escalation triggers — objective, not emotional
(1) A dependency passes its needed-by date without readiness evidence. (2) A driver cannot be met inside the agreed scope. (3) A decision crosses another team's ownership. (4) Forecast confidence falls below 60%. Each escalation states the decision or resource requested, the options with consequences, and the date by which an answer is needed.
Health
No planned weekend work; the fixed date is protected by scope, not by hours. Every module has a second person who has changed it. One team experiment per fortnight; this initiative's was “no ticket may be assigned to the person who wrote it”, kept after two cycles.
Decision rights · who decides, who advises, what reopens it
DecisionFinal callMust seek advice fromRecorded inWhat reopens it
Outcome, scope, cohort, scope tradesPriyaDana, Marco, Anya, RiskThe frame, versionedOutcome evidence, or a guardrail that cannot be protected
Experience, wording, research methodMarcoPriya, Nils, customers, accessibilityThe story map and examplesComprehension or accessibility evidence
Architecture and technical riskDanaEngineers, Ola, Asha, RaviADR logThe revisit trigger written into the ADR itself
Implementation inside agreed boundariesThe engineer doing itPeers, and any affected ownerCode and its testsA fitness function failing
Quality bar and definition of doneWhole teamNils, OlaThe charter and the rollout contractAn escaped defect class, reviewed fortnightly
Rollout advance to the next stageDana with NilsPriya, Risk, OlaThe rollout contractAny stop threshold
Rollout stop / reversalAny on-call engineer, unilaterallyNobody — advice happens afterIncident logDeliberately asymmetric: advancing needs agreement, stopping does not
Fraud guardrail breachRiskPriya, NilsThe rollout contract—
Privacy and security acceptanceOlaDanaReview sign-offA new data flow or a new retention period
Steady-state ownership acceptanceKim (platform services)Dana, NilsCloseout—

The asymmetry in row 7 is the single most useful line in the table. Advancing exposure requires two named people to agree; stopping it requires one person to act and explain afterwards. A team that needs permission to stop will not stop in time.

Traces to Every decision in this packet has an owner in this table, and the frame's “decision rights” entry points here. Risk R1 (Risk holds the fraud stop authority, not the team) · driver D4 (distributed review is what makes a one-day policy release possible) · rollout gates. Method: lesson 0048.

12 · Test, telemetry and staged rollout

Tests are chosen by risk, not by a coverage quota. Telemetry is paired: a system-health measure that is green proves nothing about whether returns resolve faster. And exposure is staged with a control cohort held back for the whole pilot, because “it improved after we launched” is not a finding.

Risk-to-test map
Risk or driverTestWhat it provesWhere it runsOwner
A1 — policy correctness200 sampled agent decisions as table-driven tests against assessEvery rule and refusal reason matches what an expert would have decidedUnit, every buildSam
D2 — 800 ms p95Load test at 40 rps against a production-shaped datasetThe decision path holds its budget under peak loadPipeline, nightlySam
D1 — carrier outageRecovery drill: carrier sandbox forced to 100% timeouts for 30 minutes, then releasedRequests are still accepted; 99% of the backlog drains within 15 minutesStaging, before every exposure stageLea
E3 — duplicate labelsContract test with an induced timeout and retry, plus a daily production reconciliation against the carrier's label listExactly one carrier label per return, in test and in productionContract + production jobLea
D3 — cross-customer disclosureNegative authorization tests on every read path; a log-scanning fitness function that fails the build on an unredacted order idNo identifier leaks through a response, an error, or a log lineContract + buildOla
D4 — one-day policy releaseImport-rule fitness function (7.3); measured lead time on the last three policy changesThe seam is real, and the claim about it is measured rather than assertedBuild + monthly reportDana
Guardrail — accessibilityAutomated axe checks in the pipeline, plus manual keyboard and screen-reader passes on the four critical flows, plus one external auditWCAG 2.2 AA on the flows that matter. Automation alone would have missed two of the three findings.Pipeline + manual + externalMarco
Unknown unknownsTwo exploratory charters per slice: “try to get a label you should not have” and “try to make the system contradict itself about a return”Behaviour the examples did not imagine. The second charter found the receipt-reversal case that became E4.Timeboxed, per sliceWhole team
D5 — reversalReversal rehearsal (below)The disable path works when a human uses it under time pressureStaging, before every stageNils
Telemetry map · every measure has an owner and a decision it can change
MeasureKindSegmented byOwnerDecision it can change
Median and p90 time from submission to decision communicatedOutcomeExposed vs control; device; decision outcomePriyaContinue / change / stop
Support-contact rate within 7 days of submissionOutcomeExposed vs control; accepted / referred / refusedPriyaContinue / change / stop; slice order
Fraud loss per 1,000 eligible returnsGuardrailExposed vs controlRiskStop (Risk's unilateral authority)
Manual review queue depth and daily arrivalsGuardrailReferral causeNilsPause the exposure ramp
Referral age; oldest unanswered referralGuardrailCauseNilsRoster change; pause the ramp
Decision-outcome mix (accepted / referred / refused by reason code)Business stateReason code; policy versionAnyaRoll back a policy change
Eligibility decision p95Health, tied to D2—SamBlock the next stage
Carrier call latency and error rate; label backlog ageHealth, tied to D1 and D5Carrier error codeLeaDisable label attempts
Age of the oldest accepted return with no terminal stateData integrity, tied to I4StateLeaEscalate to Dana after two days
Records older than 30 days in the returns schemaPrivacy—OlaHalt exposure; it is a compliance breach, not a bug
Accessibility check failures on critical flowsGuardrailFlowMarcoBlock the release

Baselines used by the measures above: eligible domestic demand 130 requests/day, rising to about 160/day from mid-November (source: order platform returns extract, rolling 12 months). Decision mix from the A1 sample and the first exposed week: 63% accepted, 26% referred, 11% refused.

Rollout contract · every stage has entry, observation, advance and reversal
StageCohortEntryObserve (minimum)Advance whenPause / reverse when · authority
1Team onlyAll tests pass; the support path exists; the runbook has a first draftAll flows, 2 daysNo critical security or accessibility defect openAny critical defect · anyone
2Staff accountsRunbook exercised; alerts route to a human; privacy sign-off; reversal rehearsal passedErrors, retries, comprehension; 50 real casesZero duplicate labels; decision p95 within 800 msAny duplicate label · Dana
31% of eligible demand, selected by a stable hash of the customer id, with a 10% holdback control that stays unexposed for the whole pilotSupport briefed; reversal rehearsed again; control cohort instrumentedOutcome and guardrails; 3 days and at least 120 exposed requestsMedian resolution improved against control; fraud not worse; queue under 50/dayFraud loss above control by more than 1.0 per 1,000 · Risk. Contact rate worse than control · Priya.
410%, then 50%Previous gate signed off by Dana and NilsCapacity, referral age, manual queue, segments (device, decision outcome); 3 days eachAll stage-3 thresholds still hold at the larger nReferral SLA missed for 4 hours · Nils. Queue above 45/day · Nils.
5100% of eligible demand, minus the 10% holdback, which is released only after the outcome has held for two consecutive weeksSteady-state owner named and has accepted the runbookContinuous; weekly reviewOutcome and guardrails hold for two consecutive weeksAny guardrail breach · the guardrail's owner

Data and side effects at each reversal: disabling exposure stops new returns entering the new path; returns already in flight continue, because abandoning them would strand customers mid-journey. Labels already bought are not voided. Refunds already requested are not recalled — see E4. Reversal is therefore “stop the intake”, never “undo the week”, and the contract says so rather than leaving the team to discover it.

Reversal rehearsal · 30 minutes, run before every stage, and what it found
Scenario
The carrier starts returning timeouts at 09:40 on a weekday. Label errors cross 2% within four minutes. The on-call engineer is someone who did not build the worker.
What the team had to demonstrate
Detect (alert fires, names the carrier), diagnose (runbook link resolves the carrier's error code), limit (disable label attempts), recover (re-enable, backlog drains), explain (a support message the agents can send).
Finding, first rehearsal (18 September)
The disable flag did not do what everyone believed. It was checked when a request created an intent, so it correctly stopped new label attempts — but the worker read the flag once at start-up and kept retrying the 63 intents already in its backlog against a failing carrier. The rehearsal's stopwatch said “disabled in 3 minutes”; the carrier's dashboard said we were still calling it eleven minutes later.
Fix
The flag is read inside the worker loop, once per intent. A test asserts that the worker stops within one iteration of the flag flipping. Added to the operability driver's evidence line, and to E3 as an explicit expectation.
Finding, second rehearsal (22 September)
Disable worked in 90 seconds. The gap was the support message: nobody could say what a customer with a pending label should be told. Written, put in the runbook, and now part of the stage-2 entry condition.
Why it is rehearsed every stage, not once
The disable path is code, and code changes. A rehearsal that happened in September proves nothing about the flag in November.

Traces to Outcome (the exposed-vs-control measures are how the outcome is read at all) · every guardrail (each has a stop threshold and a named authority) · drivers D1, D2, D3, D4, D5 (one evidence mechanism each) · assumption A6 (converted into a guardrail with a control cohort) · risk R1 · ADR 0003 (the disable flag it created is the thing being rehearsed). Method: lessons 0049 and 0060.

13 · Weekly initiative review · week 3

Week 3 is the week the evidence arrived. Three results landed within four days of each other: one changed an architecture decision, one changed the slice order, and one refused to say anything at all. This is the review as it was written — one page, built from live sources, produced in thirty minutes.

Simulated weekly review

Returns resolution · week 3 · 11 September · attending: Priya, Marco, Dana, Sam, Lea, Tomas, Ije · 30 min

One-page review
Outcome
Not yet measurable — no customer is exposed. First reading due at the 1% stage, forecast 26 September to 3 October. Reporting “on track” here would be a claim with no source.
Guardrails
One moved: the manual-queue projection is now 34 referrals/day at full exposure (68% of the 50/day cap) against a week-1 estimate of “about 12”. Others unchanged and unmeasured.
Learned
(1) The carrier is far slower and less safe under retry than its documentation says (A3, falsified). (2) The referral volume implied by the A1 sample is nearly three times the week-1 estimate. (3) Customer comprehension is still unknown; the n = 5 test was inconclusive, not positive.
Delivered
The walking skeleton runs end to end from a clean build, including the worker process and correlation ids across it. The 200-case policy suite runs and 186 of 200 pass; the 14 failures are all gift orders, which is a missing rule, not a defect. Demonstrable this Friday.
Flow
Two slices in progress, at the WIP limit. The appeal flow has not moved in three days because Marco is on the comprehension retest — this is the item the swarm rule applies to, and it is the item the scope trade below removes.
Risk and dependency
R3 (carrier latency) closes today, replaced by a smaller operational risk: worker backlog age. Payments confirmed return_id as the refund idempotency key in writing (8 Sep). Privacy review has one open finding — log retention is 3 days, not the 30 the review assumed; Ola is deciding whether that is acceptable for the audit trail.
Decisions
Two made today (below). One waiting: whether the gift-order rule enters slice 3 or is refused for the pilot — Priya, by 18 September, needs the volume from Anya.
Forecast
Pilot open to 1% between 26 September and 3 October, ~70% confidence, down from ~80% last week because the worker is six days of work that was not in last week's plan. Scope trade taken today restores about five of those days.
Ask
One decision from Ola by Monday on log retention, because the audit trail's design depends on it. No staffing change requested.

The evidence, and what each piece did

Evidence 1 · carrier spike (A3) — changed an architecture decision
Result
p50 640 ms, p95 1.9 s, p99 11.4 s. Two duplicate labels in ten timeout-retries made without a client reference.
What it falsified
Assumption A3, and with it ADR 0002, which had said the carrier call belongs inside the request. The 800 ms decision budget (D2) is unreachable at p99 11.4 s, and a 30-minute outage would have refused every eligible request, failing D1.
Changed decision
ADR 0002 is superseded by ADR 0003: persist the label intent in the accepting transaction, drain it from a worker, make the carrier call idempotent on the intent id. Signed the following day.
Dissent, recorded
Sam: at pilot volume, option C — an agent buys the label from the carrier portal — solves this with a runbook rather than a queue, a backlog metric, a flag and new on-call surface. Not resolved by argument; recorded in the ADR with the condition that would prove it right, and revisited at closeout.
Artifacts updated the same day
ADR log (0002 marked superseded, 0003 added). Container view (worker process). Dynamic view B (added). E3 (rewritten around idempotent retry). Delivery map (six days added). Rollout contract (the disable flag and its rehearsal). Risk R3 closed.
Evidence 2 · referral volume model (from A1) — changed the slice order
Result
Applying the A1 sample's 63 / 26 / 11 decision mix to the demand extract gives 34 referrals/day at full exposure, rising to about 41/day in the seasonal ramp. Week-1 planning had assumed “about 12”.
What it did not falsify
The guardrail still holds — 41 is under 50. The finding is that the headroom is nine requests a day, not thirty-eight, and that agents will be doing this work from the old helpdesk unless something changes.
Changed decision
Slice 2 becomes the agent decision view and the four-hour referral SLA. The in-product appeal flow moves behind it. A refused customer still gets a reason and a link to contact an agent; they do not get a self-service appeal until slice 3. This is exactly the scope trade the forecast pre-identified in week 1, taken now on evidence rather than under deadline pressure.
Dissent, recorded
Marco (design). “Refusal is where the contact rate lives. Sending a refused customer to a support agent to appeal is the exact behaviour the outcome measures. We are protecting the referral experience by damaging the refused one.” Decider on scope is Priya, and Priya took the trade. What would prove Marco right: if the contact rate within the refused segment exceeds 55% during the 1% stage, the appeal flow returns to the front of the queue and the exposure ramp pauses until it ships. Instrumented before the stage opens.
Artifacts updated
Story map (slice order). Delivery map (five days recovered). Rollout telemetry (contact rate now segmented by decision outcome — a measure added specifically to adjudicate a dissent). Frame unchanged: no outcome or guardrail moved.
Evidence 3 · comprehension test at n = 5 (A2) — changed nothing, on purpose
Result
Two of five participants read “your label is being prepared” as a refusal.
Honest reading
Inconclusive. Two of five is a strong-sounding fraction of a sample too small to separate a wording problem from two participants. It is not evidence that the two-step flow fails, and it is not evidence that it works.
What was done anyway
The wording changed — “Accepted. We're preparing your label; we'll email it within the hour” — because it is a one-line, fully reversible change that costs nothing if the finding was noise. A retest at n = 12 was commissioned for 15 September. The distinction is worth naming: changing something cheap and reversible on weak evidence is fine; changing something expensive and irreversible on weak evidence is not.
Why it is in the review at all
Because an inconclusive result that goes unreported becomes a silently confirmed assumption. A2 remains the largest open value risk in the packet and is on the frame's “next evidence” line.

Three results, three different responses: one decision reversed, one decision re-ordered, one decision deliberately not made. A review in which every piece of evidence conveniently supports the existing plan is a review that is being performed rather than run.

Traces to Assumptions A1, A2, A3 (the three results) · ADR 0002 and 0003 · drivers D1, D2 (the forces that made the spike decisive) · guardrail “manual queue ≤ 50/day” (the volume model) · risk R2 (Marco's dissent is R2 restated with a threshold) · story map slice order · forecast. Method: lesson 0050.

14 · Closeout

The pilot ran from 29 September to 24 November. One outcome measure was met and one was missed; both are reported with the query that produced them. Then the unglamorous part: handing the thing over, removing the machinery that was only ever meant to be temporary, and writing down what the next lead should know.

Outcome and guardrails · actual, with sources
MeasureBaselineTargetActualSourceVerdict
Median time from submission to decision communicated (eligible domestic) 72 hunder 4 h 3.1 h exposed; 69 h in the holdback control Initiative dashboard, resolution_seconds p50, 29 Sep – 24 Nov; exposed n = 6,480, control n = 694. Query in the dashboard repository, re-runnable. Met
Support-contact rate within 7 days 41%31% or lower (−25% relative) 31.6% exposed; 40.2% control. A 23% relative reduction. Same window; contact log joined on return id. Same join as the baseline query, deliberately, so the two numbers are comparable. Missed by 0.6 points. Reported as a miss, not rounded into a win.
Fraud loss per 1,000 eligible returns 2.42.4 or lower 2.2 exposed (14 events in 6,480). Control 2.5 — but see the note. Risk ledger, 29 Sep – 24 Nov, reconciled 1 December. Held on the agreed measure, with a caveat we are naming rather than hiding.
Manual review queue —(new)50/day or fewer Mean 26/day; peak 41/day on 11 November Referral queue arrivals, daily. Held, with 9/day of headroom at peak
WCAG 2.2 AA on the four critical flows —No open AA issue 3 AA issues found at the staff stage, all fixed before the 1% stage; 0 open at the final audit External audit report, 12 November. Two of the three were found by manual keyboard testing, not by the automated check. Held
Carrier cost per resolved return $3.42$3.42 or lower $3.39 Carrier invoice, October and November, divided by resolved returns. Lower because the manual process bought and voided about 3% of its labels; the idempotent path bought none twice. Held
Personal data retained no longer than 30 days —Zero records over 30 days 0 records over 30 days on 24 November Deletion job audit count, plus an independent query run by Ola on 24 November. Held
Duplicate carrier labels —Zero 0 in 6,480 returns Daily reconciliation against the carrier's own label list. Held — the invariant I2 evidence

The trajectory matters as much as the endpoint. At pilot week 4 the reading was 3.2 hours median and an 18% relative contact reduction; the remaining five points came from two rounds of wording work on the referral and refusal states, not from further speed. That is the finding recorded in the learning review below.

The weakest number in this packet, named. The fraud guardrail's control cohort contains 694 requests. At a base rate near 2.4 per 1,000 that is under two expected events, so the control cannot detect a fraud change of any plausible size — the “2.5 in control” figure is one or two incidents and should not be read as a comparison. What can honestly be said is the absolute exposed figure (2.2 per 1,000 over 6,480 requests) against a baseline computed over 4,812. The guardrail held on the measure the team agreed in advance, and the measure itself was under-powered. That is a design defect in the evidence, not in the system, and it is the first thing the steady-state owner should fix.

Who did not benefit, and what moved elsewhere
Segment or costWhat happenedDecision
Guest orders — 29% of eligible demandUntouched. The headline result covers 71% of the demand it appears to describe, and the packet says so everywhere the outcome is quoted.Sized; decision due 15 November, taken 18 November: build in Q1 with a separate fraud model. Not folded into this initiative.
Refused customers — 11% of requestsContact rate in the refused segment was 47%, above the 41% overall baseline. Being refused quickly is still being refused.Marco's week-3 dissent threshold was 55%; 47% is below it, so the appeal flow was not pulled forward — but this is the segment slice 3 exists for, and the number is now on its exit condition.
Referred customers — 26% of requestsContact rate 38%, and median resolution 3.7 h — inside the 4-hour SLA but the segment carrying almost all of the variance.Kept. The referral state is what made the automatic path safe enough to expose.
Mobile customersCompletion was 11 points below desktop in the first exposed week; 4 points below by week 8 after two rounds of wording and layout work.Closed as a gap, retained as a monitored segment.
Support operationsFront-line contact volume fell; specialist referral work rose to 26/day mean. Work moved rather than disappearing, and Nils' roster carries it.Two agents rostered permanently for referrals; agreed with head of support at handover.
Ownership transfer · accepted by name, not by email thread
AssetFromToEvidence of readinessAccepted
Service, on-call, alerts, dashboardsDanaKim (platform services)Kim's team ran the carrier-outage drill unaided on 20 November and disabled labels in 80 seconds21 Nov
Referral queue, SLA, support runbook and scriptsNils (initiative)Nils (steady state), two rostered agentsRoster published; SLA met on 97% of referrals in the final fortnight24 Nov
Eligibility policy content and the 200-case regression suiteSamAnya, with Kim's team executing changesAnya released one policy change unaided on 17 November; elapsed time 4 hours (driver D4's claim, measured)18 Nov
Carrier vendor relationship and the sandbox keyDanaAsha (platform)Key rotated into the platform vault; rotation runbook tested21 Nov
Outcome dashboard and the holdback experimentPriyaAnalytics, with Priya as readerQueries in the dashboard repository; the holdback released 24 November24 Nov
Privacy: retention job, log redaction fitness functionOlaKim, with Ola auditing quarterlyIndependent retention query run at handover24 Nov
Temporary machinery · removed, or recorded as debt with interest and a trigger
ItemDispositionOwner · date
Release flag returns_new_pathDeleted, with its fallback branch and both configurations' testsTomas · 26 Nov
Exposure percentage and holdback machineryDeleted. A holdback nobody is reading is a cohort of customers receiving a worse product for no reason.Ije · 26 Nov
Label-attempt disable flagKept — it is an operational control, not release machinery. Moved into the runbook, its off-state tested quarterly.Kim · standing
Elevated database access for three engineersRevokedAsha · 25 Nov
Pilot dashboards and the staff-account test cohortDeleted; the four steady-state panels moved to Kim's dashboardIje · 28 Nov
Manual warehouse receipt confirmation (ADR 0005)Debt, recorded. The removal trigger — 14 consecutive days under 0.1% late receipt events — was met on 9 November, but automating it was not done. Interest: about 40 clerk actions/day, roughly 1.5 hours of a person's day, indefinitely. Trigger to pay it down: the next returns initiative, or the first week the clerk's queue delays a refund past 24 hours. Owner: Kim. Reviewed quarterly.Kim · reviewed Feb
The old email-based return pathKept deliberately as the carrier-outage fallback (dynamic view B, row 7′). Documented as a fallback rather than left running as a forgotten alternative.Nils · standing
Architecture and decision hygiene at closeout
Learning review · expected / observed / surprised / next time
ExpectedObservedSurprised
Speed would drive the contact reduction.It did not, alone. The contact drop tracked proactive status more than raw speed — the referred segment resolves in 3.7 h and still contacts at 38%.Telling people what is happening is worth more than shaving hours. Would have changed the slice order in week 1.
Policy would be about 90% codifiable.63% in pilot scope. The gap was not exceptions but undocumented rules held by two people.The sample cost two analyst-days and reshaped the outcome statement. It should have been the first thing done, not the third.
The carrier would be the easy integration.It was the decision that reshaped the architecture.Vendor documentation was wrong by an order of magnitude at p99, and unsafe under retry. Measure vendors before designing around them.
The disable flag would work because it existed.It half-worked, and only a rehearsal revealed it.An untested control is a belief. The rehearsal cost 30 minutes and found a defect that would have been discovered at 02:00 during a real outage.

Two improvements owned, not twelve. (1) Every future initiative buys a domain sample before writing an outcome statement — owner Priya, applied to the guest-order initiative in Q1. (2) Every guardrail gets a power check at design time: if the agreed measure cannot detect the change it is meant to catch, it is not a guardrail — owner Risk, due before the Q1 initiative's first exposure.

The decision · continue, change, stop, or hand over
Decision
Hand over and continue in a new initiative. The returns capability moves to steady-state ownership as of 24 November. Slice 3 (opened and damaged items) and guest orders become a separate Q1 initiative with its own frame, because they need a different fraud model and a different cohort — not because they are “phase two”.
Why not simply continue
The opportunity that remains is a different opportunity. Continuing under this frame would have let the outcome statement drift to cover work it was never evidence for, which is how initiatives become permanent programmes with unfalsifiable goals.
Why not stop
Both measures moved substantially against a held-back control, and every guardrail held. The contact-rate miss is 0.6 points on a measure that moved 9.4 points; it is a miss worth reporting, not a reason to reverse.
What the next lead is told
Read ADR 0003 before touching the label path. The fraud guardrail is under-powered — fix the measure before you widen the cohort. The manual receipt debt has a trigger that has already fired. And the outcome statement covers 71% of demand: quote it with that clause attached or you will mislead someone.

Traces to The frame's outcome and every guardrail, each reported against its own baseline and source · assumptions A1, A2, A3, A6 (what each turned out to be worth) · ADR 0003's accepted costs and Sam's dissent, both revisited · the week-3 dissent from Marco, adjudicated against its own threshold · the rollout contract's flags and cohorts, now removed. Method: lesson 0050.

The traceability line

The test of the packet is not its length. It is whether one line runs through all fourteen artifacts without breaking. Here is that line, read end to end for a single thread:

One line, fourteen artifacts

Outcome “median under 4 hours” → assumption A3 “the carrier is fast enough to sit inside the request” → target workflow puts the label after the decision → pilot slice includes a label the customer can actually use → example E3 pins what a timeout means → drivers D1 and D2 give it a number and a failure condition → container view gains a worker process and dynamic view B → state table makes “exactly one label per return” a constraint, not a convention → ADR 0003 chooses the mechanism, records Sam's dissent and its accepted costs → delivery map puts the carrier port in the walking skeleton in week 2 → decision rights say Dana decides it and any on-call engineer may stop it → rollout rehearses the disable path and finds it broken → week-3 review is where the evidence arrived and the decision changed → closeout reports zero duplicate labels in 6,480 returns, hands the worker to Kim, and revisits the dissent.

Now try it on your own packet. Pick your riskiest assumption and walk it forward. The first place the line breaks is either work nobody needed or a risk nobody is carrying.

Before you compare
  1. Which artifact in this packet would you have skipped, and which decision would you then have made by instinct?
  2. Two decisions changed under evidence and two dissents were recorded. In your last initiative, where is that written down?
  3. The contact-rate target was missed by 0.6 points and reported as a miss. What would have happened to that number in your organisation?

Next: the same initiative, done weakly — realistic, plausible, and approved in a great many companies. Then the reviewer notes, which put the two side by side and name the rubric dimension each difference touches.