Files
doormilxpress_astryx/DOORMILE_DOSSIER.html
2026-08-29 15:42:56 +05:30

1710 lines
150 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<style>
:root { color-scheme: light dark; }
html, body { margin: 0; }
img { max-width: 100%; }
[hidden] { display: none !important; }
</style>
<title>Doormile Platform Dossier</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Archivo:wght@500;600;700&family=Source+Sans+3:ital,wght@0,400;0,600;1,400&family=JetBrains+Mono:wght@400;600&display=swap">
<style>
:root {
--paper:#F6F4F5; --surface:#FFFFFF; --surface-2:#EFEBED;
--ink:#17141A; --ink-2:#453E48; --muted:#6E6670;
--line:#DCD5D8; --line-2:#C6BCC0;
--accent:#C01227; --accent-ink:#C01227; --accent-soft:#F6E2E5;
--alt:#10646F; --alt-soft:#DDEEF0;
--gold:#8A6206; --gold-soft:#F7ECD4;
--shadow: 0 1px 2px rgba(23,20,26,.06), 0 8px 24px -16px rgba(23,20,26,.35);
}
@media (prefers-color-scheme: dark) {
:root:not([data-theme="light"]) {
--paper:#121014; --surface:#1A171C; --surface-2:#221E24;
--ink:#EFEAEC; --ink-2:#C4BBC0; --muted:#968D94;
--line:#322C34; --line-2:#443C46;
--accent:#FF6076; --accent-ink:#FF8090; --accent-soft:#3A1B22;
--alt:#4FC7D4; --alt-soft:#10333A;
--gold:#E8B84B; --gold-soft:#3A2E10;
--shadow: 0 1px 2px rgba(0,0,0,.5), 0 8px 24px -16px rgba(0,0,0,.9);
}
}
:root[data-theme="dark"] {
--paper:#121014; --surface:#1A171C; --surface-2:#221E24;
--ink:#EFEAEC; --ink-2:#C4BBC0; --muted:#968D94;
--line:#322C34; --line-2:#443C46;
--accent:#FF6076; --accent-ink:#FF8090; --accent-soft:#3A1B22;
--alt:#4FC7D4; --alt-soft:#10333A;
--gold:#E8B84B; --gold-soft:#3A2E10;
--shadow: 0 1px 2px rgba(0,0,0,.5), 0 8px 24px -16px rgba(0,0,0,.9);
}
* { box-sizing: border-box; }
body {
background: var(--paper); color: var(--ink);
font-family: "Source Sans 3", ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif;
font-size: 16.5px; line-height: 1.62; margin: 0; padding: 0 24px 96px;
-webkit-font-smoothing: antialiased;
}
.wrap { max-width: 1180px; margin: 0 auto; }
.measure { max-width: 68ch; }
header.masthead { padding: 64px 0 40px; border-bottom: 2px solid var(--ink); margin-bottom: 40px; }
.eyebrow {
font-family: "JetBrains Mono", ui-monospace, Menlo, monospace;
font-size: 11.5px; letter-spacing: .14em; text-transform: uppercase;
color: var(--accent-ink); margin: 0 0 18px;
}
h1 {
font-family: Archivo, "Helvetica Neue", Arial, sans-serif; font-weight: 700;
font-size: clamp(2.3rem, 5.4vw, 3.6rem); line-height: 1.03;
letter-spacing: -.03em; text-wrap: balance; margin: 0 0 20px;
}
.standfirst { font-size: 1.16rem; color: var(--ink-2); max-width: 62ch; margin: 0; }
.standfirst strong { color: var(--ink); font-weight: 600; }
nav.toc { display: flex; flex-wrap: wrap; gap: 8px; margin: 0 0 52px; }
nav.toc a {
font-family: "JetBrains Mono", monospace; font-size: 11.5px; letter-spacing: .05em;
text-decoration: none; color: var(--ink-2); background: var(--surface);
border: 1px solid var(--line); border-radius: 3px; padding: 6px 11px;
}
nav.toc a:hover { border-color: var(--accent); color: var(--accent-ink); }
nav.toc a:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
section { margin: 0 0 76px; scroll-margin-top: 20px; }
h2 {
font-family: Archivo, "Helvetica Neue", Arial, sans-serif; font-weight: 600;
font-size: 1.62rem; letter-spacing: -.017em; line-height: 1.2;
text-wrap: balance; margin: 0 0 6px;
}
h3 {
font-family: Archivo, "Helvetica Neue", Arial, sans-serif; font-weight: 600;
font-size: 1.1rem; letter-spacing: -.01em; margin: 34px 0 8px;
}
.sec-head {
display: flex; align-items: baseline; gap: 16px;
border-top: 1px solid var(--line); padding-top: 16px; margin-bottom: 18px;
}
.sec-num {
font-family: "JetBrains Mono", monospace; font-size: 12px; font-weight: 600;
color: var(--accent-ink); padding-top: 6px; flex: none;
}
p { margin: 0 0 15px; }
code, .mono { font-family: "JetBrains Mono", ui-monospace, Menlo, monospace; font-size: .86em; }
p code, li code, td code {
background: var(--surface-2); border: 1px solid var(--line);
border-radius: 3px; padding: .07em .34em; color: var(--ink);
}
figure {
margin: 26px 0 32px; background: var(--surface); border: 1px solid var(--line);
border-radius: 4px; box-shadow: var(--shadow); overflow: hidden;
}
.fig-scroll { overflow-x: auto; padding: 22px 22px 6px; }
figure svg { display: block; max-width: 100%; height: auto; min-width: 700px; margin: 0 auto; }
figcaption {
border-top: 1px solid var(--line); background: var(--surface-2);
padding: 12px 22px 14px; font-size: 14px; color: var(--ink-2); line-height: 1.5;
}
figcaption b {
font-family: "JetBrains Mono", monospace; font-size: 11px; letter-spacing: .1em;
text-transform: uppercase; color: var(--accent-ink); display: block;
margin-bottom: 4px; font-weight: 600;
}
.table-scroll { overflow-x: auto; margin: 24px 0; border: 1px solid var(--line); border-radius: 4px; background: var(--surface); }
table { border-collapse: collapse; width: 100%; font-size: 14.4px; min-width: 720px; }
th, td { text-align: left; padding: 11px 15px; border-bottom: 1px solid var(--line); vertical-align: top; }
thead th {
font-family: "JetBrains Mono", monospace; font-size: 11px; letter-spacing: .1em;
text-transform: uppercase; color: var(--muted); font-weight: 600; background: var(--surface-2);
}
tbody tr:last-child td { border-bottom: 0; }
td.num { font-family: "JetBrains Mono", monospace; font-variant-numeric: tabular-nums; white-space: nowrap; }
.pill {
font-family: "JetBrains Mono", monospace; font-size: 10.5px; padding: 2px 7px;
border-radius: 3px; white-space: nowrap; font-weight: 600;
}
.p-high { background: var(--alt-soft); color: var(--alt); }
.p-mid { background: var(--gold-soft); color: var(--gold); }
.p-low { background: var(--surface-2); color: var(--muted); }
.callout {
border-left: 3px solid var(--accent); background: var(--accent-soft);
padding: 16px 20px; border-radius: 0 4px 4px 0; margin: 24px 0; font-size: 15.4px;
}
.callout.teal { border-left-color: var(--alt); background: var(--alt-soft); }
.callout.teal .label { color: var(--alt); }
.callout.gold { border-left-color: var(--gold); background: var(--gold-soft); }
.callout.gold .label { color: var(--gold); }
.callout p:last-child { margin-bottom: 0; }
.callout .label {
font-family: "JetBrains Mono", monospace; font-size: 11px; letter-spacing: .1em;
text-transform: uppercase; font-weight: 600; color: var(--accent-ink);
display: block; margin-bottom: 5px;
}
ul.plain { padding-left: 1.15em; margin: 12px 0 18px; max-width: 68ch; }
ul.plain li { margin-bottom: 8px; }
pre {
background: var(--surface-2); border: 1px solid var(--line); border-radius: 4px;
padding: 14px 16px; overflow-x: auto; font-size: 13px; line-height: 1.55;
font-family: "JetBrains Mono", ui-monospace, Menlo, monospace; color: var(--ink);
margin: 18px 0;
}
footer { border-top: 1px solid var(--line); padding-top: 20px; color: var(--muted); font-size: 13.6px; }
@media (max-width: 640px) {
body { padding: 0 16px 64px; font-size: 16px; }
header.masthead { padding: 44px 0 30px; }
}
/* ---- merged-document additions ---- */
h3 .leg {
font-family: "JetBrains Mono", monospace; font-size: 11px; letter-spacing: .1em;
text-transform: uppercase; padding: 2px 7px; border-radius: 3px; margin-right: 9px;
vertical-align: 2px; font-weight: 600;
}
.leg-1 { background: var(--accent-soft); color: var(--accent-ink); }
.leg-2 { background: var(--gold-soft); color: var(--gold); }
.leg-3 { background: var(--alt-soft); color: var(--alt); }
.sec-head .tag {
font-family: "JetBrains Mono", monospace; font-size: 10.5px; letter-spacing: .1em;
text-transform: uppercase; padding: 3px 9px; border-radius: 3px; font-weight: 600; flex: none;
}
.t-first { background: var(--accent-soft); color: var(--accent-ink); }
.t-mid { background: var(--gold-soft); color: var(--gold); }
.t-last { background: var(--alt-soft); color: var(--alt); }
.t-all { background: var(--surface-2); color: var(--ink-2); }
.wrap { max-width: 1340px; }
.shell { display: block; }
aside.rail { margin: 0 0 48px; }
.rail-group { margin-bottom: 18px; }
.rail-group > .rg-head {
font-family: "JetBrains Mono", monospace; font-size: 10.5px; letter-spacing: .12em;
text-transform: uppercase; color: var(--accent-ink); font-weight: 600;
margin: 0 0 8px; display: block;
}
.rail-group ol { list-style: none; margin: 0; padding: 0; display: flex; flex-wrap: wrap; gap: 6px; }
.rail-group a {
display: block; text-decoration: none; color: var(--ink-2);
background: var(--surface); border: 1px solid var(--line); border-radius: 3px;
padding: 5px 10px; font-size: 13px; line-height: 1.3;
}
.rail-group a .n {
font-family: "JetBrains Mono", monospace; font-size: 10.5px; color: var(--muted); margin-right: 7px;
}
.rail-group a:hover { border-color: var(--accent); color: var(--accent-ink); }
.rail-group a:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.part {
margin: 0 0 40px; padding: 30px 0 0; border-top: 3px solid var(--ink);
}
.part-kicker {
font-family: "JetBrains Mono", monospace; font-size: 11px; letter-spacing: .16em;
text-transform: uppercase; color: var(--accent-ink); font-weight: 600; margin: 0 0 12px;
}
.part h2.part-title {
font-family: Archivo, "Helvetica Neue", Arial, sans-serif; font-weight: 700;
font-size: clamp(1.7rem, 3.6vw, 2.5rem); letter-spacing: -.025em; line-height: 1.06;
margin: 0 0 14px; text-wrap: balance;
}
.part .part-intro { font-size: 1.06rem; color: var(--ink-2); max-width: 64ch; margin: 0; }
.part .part-intro strong { color: var(--ink); font-weight: 600; }
@media (min-width: 1120px) {
.shell { display: grid; grid-template-columns: 236px minmax(0, 1fr); gap: 56px; align-items: start; }
aside.rail { position: sticky; top: 20px; max-height: calc(100vh - 40px); overflow-y: auto; margin: 0; }
.rail-group ol { display: block; }
.rail-group a { border: 0; border-left: 2px solid var(--line); border-radius: 0; padding: 4px 0 4px 11px; background: none; margin-bottom: 1px; }
.rail-group a:hover { border-left-color: var(--accent); background: none; }
}
</style>
</head>
<body>
<div class="wrap">
<header class="masthead">
<p class="eyebrow">Architecture · operations · integration plan</p>
<h1>Doormile Platform Dossier</h1>
<p class="standfirst">The whole review in one place: how a parcel actually moves through Doormile, each leg drawn on its own, and where generative AI belongs in the platform. Nineteen sections, twelve diagrams, read against the <strong>nearle-de</strong> source.</p>
</header>
<div class="shell">
<aside class="rail">
<div class="rail-group">
<span class="rg-head">Part I · Architecture</span>
<ol>
<li><a href="#i-lifecycle"><span class="n">01</span>The parcel's path, end to end</a></li>
<li><a href="#i-seam"><span class="n">02</span>Where the model and the code disagree</a></li>
<li><a href="#i-waves"><span class="n">03</span>Why a wave isn't a round</a></li>
<li><a href="#i-native"><span class="n">04</span>What a native milk round would need</a></li>
</ol>
</div>
<div class="rail-group">
<span class="rg-head">Part II · The three legs</span>
<ol>
<li><a href="#first"><span class="n">01</span>Shipper to origin hub</a></li>
<li><a href="#mid"><span class="n">02</span>Hub to hub</a></li>
<li><a href="#last"><span class="n">03</span>Destination hub to consignee</a></li>
<li><a href="#whole"><span class="n">04</span>The whole flow</a></li>
<li><a href="#ordering"><span class="n">05</span>The ordering mistake</a></li>
<li><a href="#clocks"><span class="n">06</span>Put the intelligence where it belongs</a></li>
<li><a href="#build"><span class="n">07</span>Build order</a></li>
</ol>
</div>
<div class="rail-group">
<span class="rg-head">Part III · Agent layer</span>
<ol>
<li><a href="#rule"><span class="n">00</span>The rule that makes all of this work</a></li>
<li><a href="#map"><span class="n">01</span>Where GenAI actually earns its place</a></li>
<li><a href="#assign"><span class="n">02</span>Auto-assign, properly</a></li>
<li><a href="#wiring"><span class="n">03</span>Wiring it into what you have</a></li>
<li><a href="#ladder"><span class="n">04</span>The trust ladder</a></li>
<li><a href="#loop"><span class="n">05</span>The loop you already built</a></li>
<li><a href="#guard"><span class="n">06</span>Guardrails</a></li>
<li><a href="#plan"><span class="n">07</span>Ninety days</a></li>
</ol>
</div>
</aside>
<main>
<div class="part">
<p class="part-kicker">Part I</p>
<h2 class="part-title">Doormile Parcel Flow</h2>
<p class="part-intro">How a parcel actually moves through Doormile — from a sales rep's GPS-stamped doorstep survey to a consignee reading six digits aloud. And where that pipeline, built for <strong>on-demand courier</strong>, meets a business run as <strong>milk rounds</strong>.</p>
</div>
<section id="i-lifecycle">
<div class="sec-head">
<span class="sec-num">01</span>
<div>
<h2>The parcel's path, end to end</h2>
</div>
</div>
<p class="measure">Seven stages, two route branches, one convergence. Every arrow below is a real state transition in the Go backend — the labels are the endpoints that cause them and the statuses they write.</p>
<figure>
<div class="fig-scroll">
<svg viewBox="0 0 1120 1268" role="img" aria-label="End-to-end Doormile lifecycle: field survey creates a client that converts to a tenant with a rate card; an order from one of three sources becomes a PickupBooking; Redis geo-search plus Claude scoring assigns a miler; the miler accepts, reaches and completes pickup, converting the booking into a Consignment with a receiver-only OTP; matching three-digit pincode prefixes route hyperlocal straight to out-for-delivery, everything else via hub, tripsheet and linehaul; both branches close on OTP verification and a stored delivery proof.">
<defs>
<marker id="I_ar" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse">
<path d="M0,0 L10,5 L0,10 z" fill="currentColor"/>
</marker>
<marker id="I_ar-a" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse">
<path d="M0,0 L10,5 L0,10 z" fill="var(--accent)"/>
</marker>
<marker id="I_ar-t" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse">
<path d="M0,0 L10,5 L0,10 z" fill="var(--alt)"/>
</marker>
</defs>
<g font-family="'Source Sans 3', sans-serif" color="var(--line-2)">
<!-- ============ STAGE 1 ============ -->
<text x="0" y="34" font-family="'JetBrains Mono', monospace" font-size="10.5" font-weight="600" letter-spacing="1.2" fill="var(--muted)">01 · ACQUISITION</text>
<line x1="0" y1="44" x2="1120" y2="44" stroke="var(--line)" stroke-width="1"/>
<rect x="0" y="62" width="196" height="62" rx="3" fill="var(--surface-2)" stroke="var(--line-2)"/>
<text x="98" y="88" text-anchor="middle" font-size="13" font-weight="600" fill="var(--ink)">Field rep on site</text>
<text x="98" y="106" text-anchor="middle" font-size="10.5" font-family="'JetBrains Mono', monospace" fill="var(--muted)">surveylat / surveylong</text>
<line x1="200" y1="93" x2="246" y2="93" stroke="currentColor" stroke-width="1.4" marker-end="url(#I_ar)"/>
<rect x="250" y="62" width="212" height="62" rx="3" fill="var(--surface-2)" stroke="var(--line-2)"/>
<text x="356" y="88" text-anchor="middle" font-size="13" font-weight="600" fill="var(--ink)">DoormileClient</text>
<text x="356" y="106" text-anchor="middle" font-size="10.5" font-family="'JetBrains Mono', monospace" fill="var(--muted)">POST /crm/clients</text>
<line x1="466" y1="93" x2="524" y2="93" stroke="currentColor" stroke-width="1.4" marker-end="url(#I_ar)"/>
<text x="495" y="84" text-anchor="middle" font-size="10" fill="var(--muted)">converts</text>
<rect x="528" y="62" width="180" height="62" rx="3" fill="var(--surface-2)" stroke="var(--line-2)"/>
<text x="618" y="88" text-anchor="middle" font-size="13" font-weight="600" fill="var(--ink)">Tenant</text>
<text x="618" y="106" text-anchor="middle" font-size="10.5" font-family="'JetBrains Mono', monospace" fill="var(--muted)">billable account</text>
<line x1="712" y1="93" x2="758" y2="93" stroke="currentColor" stroke-width="1.4" marker-end="url(#I_ar)"/>
<rect x="762" y="62" width="212" height="62" rx="3" fill="var(--surface-2)" stroke="var(--line-2)"/>
<text x="868" y="86" text-anchor="middle" font-size="13" font-weight="600" fill="var(--ink)">TenantPricing</text>
<text x="868" y="105" text-anchor="middle" font-size="10.5" font-family="'JetBrains Mono', monospace" fill="var(--muted)">+ TenantLocation sites</text>
<!-- ============ STAGE 2 ============ -->
<text x="0" y="176" font-family="'JetBrains Mono', monospace" font-size="10.5" font-weight="600" letter-spacing="1.2" fill="var(--muted)">02 · ORIGINATION</text>
<line x1="0" y1="186" x2="1120" y2="186" stroke="var(--line)" stroke-width="1"/>
<rect x="0" y="204" width="196" height="44" rx="3" fill="var(--surface)" stroke="var(--line-2)"/>
<text x="98" y="231" text-anchor="middle" font-size="12.5" fill="var(--ink)">Customer App</text>
<rect x="0" y="256" width="196" height="44" rx="3" fill="var(--surface)" stroke="var(--line-2)"/>
<text x="98" y="283" text-anchor="middle" font-size="12.5" fill="var(--ink)">CRM Console</text>
<rect x="0" y="308" width="196" height="44" rx="3" fill="var(--surface)" stroke="var(--line-2)"/>
<text x="98" y="329" text-anchor="middle" font-size="12.5" fill="var(--ink)">Express Console</text>
<text x="98" y="344" text-anchor="middle" font-size="10" font-family="'JetBrains Mono', monospace" fill="var(--muted)">bulk Excel upload</text>
<path d="M200,226 H236 V278" fill="none" stroke="currentColor" stroke-width="1.3"/>
<path d="M200,278 H236" fill="none" stroke="currentColor" stroke-width="1.3"/>
<path d="M200,330 H236 V278" fill="none" stroke="currentColor" stroke-width="1.3"/>
<line x1="236" y1="278" x2="292" y2="278" stroke="currentColor" stroke-width="1.4" marker-end="url(#I_ar)"/>
<rect x="296" y="244" width="378" height="68" rx="3" fill="var(--surface-2)" stroke="var(--ink)" stroke-width="1.5"/>
<text x="485" y="271" text-anchor="middle" font-size="14" font-weight="600" fill="var(--ink)">PickupBooking</text>
<text x="485" y="292" text-anchor="middle" font-size="10.5" font-family="'JetBrains Mono', monospace" fill="var(--muted)">status: Pending_Pickup · priced vs TenantPricing</text>
<line x1="678" y1="278" x2="722" y2="278" stroke="var(--alt)" stroke-width="1.4" stroke-dasharray="4 3" marker-end="url(#I_ar-t)"/>
<rect x="726" y="248" width="248" height="60" rx="3" fill="var(--alt-soft)" stroke="var(--alt)" stroke-dasharray="4 3"/>
<text x="850" y="272" text-anchor="middle" font-size="11.5" font-weight="600" fill="var(--alt)">Preferredpickupfrom / to</text>
<text x="850" y="290" text-anchor="middle" font-size="10.5" fill="var(--alt)">the time-window field — unused as a round</text>
<!-- ============ STAGE 3 ============ -->
<text x="0" y="382" font-family="'JetBrains Mono', monospace" font-size="10.5" font-weight="600" letter-spacing="1.2" fill="var(--muted)">03 · DISPATCH</text>
<line x1="0" y1="392" x2="1120" y2="392" stroke="var(--line)" stroke-width="1"/>
<rect x="0" y="412" width="204" height="64" rx="3" fill="var(--surface-2)" stroke="var(--line-2)"/>
<text x="102" y="437" text-anchor="middle" font-size="12.5" font-weight="600" fill="var(--ink)">Redis GEOSEARCH</text>
<text x="102" y="456" text-anchor="middle" font-size="10.5" font-family="'JetBrains Mono', monospace" fill="var(--muted)">milers:locations · 10 km</text>
<line x1="208" y1="444" x2="252" y2="444" stroke="currentColor" stroke-width="1.4" marker-end="url(#I_ar)"/>
<text x="230" y="435" text-anchor="middle" font-size="9.5" fill="var(--muted)">candidates</text>
<rect x="256" y="404" width="238" height="80" rx="3" fill="var(--accent-soft)" stroke="var(--accent)"/>
<text x="375" y="428" text-anchor="middle" font-size="13" font-weight="600" fill="var(--accent-ink)">Claude Sonnet scoring</text>
<text x="375" y="446" text-anchor="middle" font-size="10.5" fill="var(--accent-ink)">distance · load · rating</text>
<text x="375" y="463" text-anchor="middle" font-size="10.5" fill="var(--accent-ink)">30-day on-time record</text>
<line x1="375" y1="508" x2="375" y2="488" stroke="var(--accent)" stroke-width="1.3" marker-end="url(#I_ar-a)"/>
<rect x="256" y="510" width="238" height="34" rx="3" fill="var(--surface)" stroke="var(--line-2)"/>
<text x="375" y="532" text-anchor="middle" font-size="11" font-family="'JetBrains Mono', monospace" fill="var(--muted)">pgvector RAG memory</text>
<line x1="498" y1="444" x2="542" y2="444" stroke="currentColor" stroke-width="1.4" marker-end="url(#I_ar)"/>
<rect x="546" y="412" width="184" height="64" rx="3" fill="var(--surface-2)" stroke="var(--line-2)"/>
<text x="638" y="437" text-anchor="middle" font-size="12.5" font-weight="600" fill="var(--ink)">AgentDecision</text>
<text x="638" y="456" text-anchor="middle" font-size="10.5" font-family="'JetBrains Mono', monospace" fill="var(--muted)">reasoning logged</text>
<line x1="734" y1="444" x2="778" y2="444" stroke="currentColor" stroke-width="1.4" marker-end="url(#I_ar)"/>
<rect x="782" y="412" width="192" height="64" rx="3" fill="var(--surface-2)" stroke="var(--ink)" stroke-width="1.5"/>
<text x="878" y="437" text-anchor="middle" font-size="12.5" font-weight="600" fill="var(--ink)">BookingAssignment</text>
<text x="878" y="456" text-anchor="middle" font-size="10.5" font-family="'JetBrains Mono', monospace" fill="var(--muted)">Miler_Assigned</text>
<path d="M102,480 V566 H878 V480" fill="none" stroke="var(--muted)" stroke-width="1.2" stroke-dasharray="5 4" marker-end="url(#I_ar)"/>
<rect x="380" y="554" width="220" height="24" rx="3" fill="var(--surface)" stroke="var(--line)"/>
<text x="490" y="571" text-anchor="middle" font-size="10.5" fill="var(--muted)">5 s timeout → greedy nearest miler</text>
<!-- ============ STAGE 4 ============ -->
<text x="0" y="616" font-family="'JetBrains Mono', monospace" font-size="10.5" font-weight="600" letter-spacing="1.2" fill="var(--muted)">04 · FIRST MILE</text>
<line x1="0" y1="626" x2="1120" y2="626" stroke="var(--line)" stroke-width="1"/>
<rect x="0" y="646" width="286" height="62" rx="3" fill="var(--surface-2)" stroke="var(--line-2)"/>
<text x="143" y="671" text-anchor="middle" font-size="12.5" font-weight="600" fill="var(--ink)">Accept</text>
<text x="143" y="690" text-anchor="middle" font-size="10" font-family="'JetBrains Mono', monospace" fill="var(--muted)">/assignments/:id/accept</text>
<line x1="290" y1="677" x2="334" y2="677" stroke="currentColor" stroke-width="1.4" marker-end="url(#I_ar)"/>
<rect x="338" y="646" width="286" height="62" rx="3" fill="var(--surface-2)" stroke="var(--line-2)"/>
<text x="481" y="671" text-anchor="middle" font-size="12.5" font-weight="600" fill="var(--ink)">Reached doorstep</text>
<text x="481" y="690" text-anchor="middle" font-size="10" font-family="'JetBrains Mono', monospace" fill="var(--muted)">arrivedat + arrival GPS</text>
<line x1="628" y1="677" x2="672" y2="677" stroke="currentColor" stroke-width="1.4" marker-end="url(#I_ar)"/>
<rect x="676" y="646" width="298" height="62" rx="3" fill="var(--surface-2)" stroke="var(--line-2)"/>
<text x="825" y="671" text-anchor="middle" font-size="12.5" font-weight="600" fill="var(--ink)">Pickup complete</text>
<text x="825" y="690" text-anchor="middle" font-size="10" font-family="'JetBrains Mono', monospace" fill="var(--muted)">COD collected · Idempotency-Key</text>
<!-- ============ STAGE 5 ============ -->
<text x="0" y="748" font-family="'JetBrains Mono', monospace" font-size="10.5" font-weight="600" letter-spacing="1.2" fill="var(--muted)">05 · CUSTODY HANDOFF</text>
<line x1="0" y1="758" x2="1120" y2="758" stroke="var(--line)" stroke-width="1"/>
<rect x="0" y="778" width="490" height="72" rx="3" fill="var(--surface-2)" stroke="var(--ink)" stroke-width="1.5"/>
<text x="245" y="806" text-anchor="middle" font-size="14" font-weight="600" fill="var(--ink)">Consignment created</text>
<text x="245" y="827" text-anchor="middle" font-size="10.5" font-family="'JetBrains Mono', monospace" fill="var(--muted)">Collected_By_Miler · tracking number issued</text>
<line x1="494" y1="814" x2="538" y2="814" stroke="currentColor" stroke-width="1.4" marker-end="url(#I_ar)"/>
<rect x="542" y="778" width="432" height="72" rx="3" fill="var(--accent-soft)" stroke="var(--accent)"/>
<text x="758" y="806" text-anchor="middle" font-size="14" font-weight="600" fill="var(--accent-ink)">6-digit delivery OTP</text>
<text x="758" y="827" text-anchor="middle" font-size="10.5" fill="var(--accent-ink)">to the receiver only — stripped from rider &amp; console APIs</text>
<!-- ============ STAGE 6 ============ -->
<text x="0" y="890" font-family="'JetBrains Mono', monospace" font-size="10.5" font-weight="600" letter-spacing="1.2" fill="var(--muted)">06 · ROUTE FORK</text>
<line x1="0" y1="900" x2="1120" y2="900" stroke="var(--line)" stroke-width="1"/>
<rect x="300" y="918" width="380" height="46" rx="23" fill="var(--surface)" stroke="var(--ink)" stroke-width="1.5"/>
<text x="490" y="946" text-anchor="middle" font-size="12.5" font-weight="600" fill="var(--ink)">first 3 pincode digits match?</text>
<path d="M300,941 H180 V1000" fill="none" stroke="var(--alt)" stroke-width="1.5" marker-end="url(#I_ar-t)"/>
<text x="196" y="978" font-size="10.5" font-weight="600" fill="var(--alt)">YES · hyperlocal</text>
<path d="M680,941 H800 V1000" fill="none" stroke="currentColor" stroke-width="1.5" marker-end="url(#I_ar)"/>
<text x="700" y="978" font-size="10.5" font-weight="600" fill="var(--muted)">NO · hub &amp; spoke</text>
<!-- left branch -->
<rect x="0" y="1002" width="360" height="42" rx="3" fill="var(--alt-soft)" stroke="var(--alt)"/>
<text x="180" y="1029" text-anchor="middle" font-size="11.5" font-family="'JetBrains Mono', monospace" fill="var(--alt)">/consignments/:id/start-delivery</text>
<line x1="180" y1="1046" x2="180" y2="1062" stroke="var(--alt)" stroke-width="1.4" marker-end="url(#I_ar-t)"/>
<rect x="0" y="1064" width="360" height="42" rx="3" fill="var(--alt-soft)" stroke="var(--alt)"/>
<text x="180" y="1091" text-anchor="middle" font-size="12" font-weight="600" fill="var(--alt)">Out_for_Delivery</text>
<!-- right branch -->
<rect x="620" y="1002" width="360" height="42" rx="3" fill="var(--surface-2)" stroke="var(--line-2)"/>
<text x="800" y="1029" text-anchor="middle" font-size="11.5" fill="var(--ink)">inbound scan → <tspan font-family="'JetBrains Mono', monospace" font-size="10.5">Inwarded_at_Hub</tspan></text>
<line x1="800" y1="1046" x2="800" y2="1062" stroke="currentColor" stroke-width="1.4" marker-end="url(#I_ar)"/>
<rect x="620" y="1064" width="360" height="42" rx="3" fill="var(--surface-2)" stroke="var(--line-2)"/>
<text x="800" y="1091" text-anchor="middle" font-size="11.5" fill="var(--ink)">tripsheet loaded → <tspan font-family="'JetBrains Mono', monospace" font-size="10.5">In_Transit</tspan></text>
<line x1="800" y1="1108" x2="800" y2="1124" stroke="currentColor" stroke-width="1.4" marker-end="url(#I_ar)"/>
<rect x="620" y="1126" width="360" height="42" rx="3" fill="var(--surface-2)" stroke="var(--line-2)"/>
<text x="800" y="1153" text-anchor="middle" font-size="11.5" fill="var(--ink)">destination hub arrive → assign-miler</text>
<!-- ============ STAGE 7 ============ -->
<path d="M180,1108 V1206 H420" fill="none" stroke="var(--alt)" stroke-width="1.5" marker-end="url(#I_ar-t)"/>
<path d="M800,1170 V1206 H560" fill="none" stroke="currentColor" stroke-width="1.5" marker-end="url(#I_ar)"/>
<rect x="290" y="1182" width="540" height="66" rx="3" fill="var(--surface-2)" stroke="var(--ink)" stroke-width="2"/>
<text x="560" y="1210" text-anchor="middle" font-size="14" font-weight="600" fill="var(--ink)">OTP verified → DeliveryProof stored</text>
<text x="560" y="1231" text-anchor="middle" font-size="10.5" font-family="'JetBrains Mono', monospace" fill="var(--muted)">signature · photo · geolocation → Delivered</text>
</g>
</svg>
</div>
<figcaption><b>Figure I.1 — the lifecycle</b>The booking is a promise to collect; the consignment is a parcel in custody. <code>pickup-complete</code> is the pivot between them, and the only place the OTP is minted. The teal branch never touches a hub.</figcaption>
</figure>
<ol class="stages">
<li><span class="n">01</span><span><b>Acquisition.</b> <span class="d">A rep registers a business on site; the survey coordinates are the proof they stood there. The lead converts to a Tenant with its own rate card and depot sites.</span></span></li>
<li><span class="n">02</span><span><b>Origination.</b> <span class="d">Three doors, one object. <code>Bookingsource</code> is the only thing that distinguishes them.</span></span></li>
<li><span class="n">03</span><span><b>Dispatch.</b> <span class="d">Nearby available riders from Redis, scored by Claude with reasoning logged, degrading to greedy-nearest after five seconds.</span></span></li>
<li><span class="n">04</span><span><b>First mile.</b> <span class="d">Accept, reach, collect — each an idempotent state transition, because riders work on flaky mobile networks.</span></span></li>
<li><span class="n">05</span><span><b>Custody handoff.</b> <span class="d">Booking becomes Consignment. A six-digit code goes to the receiver and to nobody else.</span></span></li>
<li><span class="n">06</span><span><b>Route fork.</b> <span class="d">Matching 3-digit pincode prefixes skip the hub entirely. Everything else rides a tripsheet.</span></span></li>
<li><span class="n">07</span><span><b>Close.</b> <span class="d">The consignee reads six digits aloud. Nothing else marks a parcel delivered.</span></span></li>
</ol>
</section>
<section id="i-seam">
<div class="sec-head">
<span class="sec-num">02</span>
<div><h2>Where the model and the code disagree</h2></div>
</div>
<p class="measure">A milk round has five properties, and all five are anti-on-demand: a fixed beat, a rider who owns it, standing orders that recur by default, a promised slot, and density economics. Stage 03 above is built on the opposite premise.</p>
<figure>
<div class="fig-scroll">
<svg viewBox="0 0 1120 452" role="img" aria-label="Left: today, four parcels each independently scored and assigned to whichever rider is nearest, producing crossing assignment lines and a rider set that changes per parcel. Right: the milkman model, one rider owning one ordered sequence of stops that repeats daily, with a substitution table covering absence.">
<defs>
<marker id="I_ar2" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="6" markerHeight="6" orient="auto-start-reverse">
<path d="M0,0 L10,5 L0,10 z" fill="currentColor"/>
</marker>
<marker id="I_ar2t" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="6" markerHeight="6" orient="auto-start-reverse">
<path d="M0,0 L10,5 L0,10 z" fill="var(--alt)"/>
</marker>
</defs>
<g font-family="'Source Sans 3', sans-serif" color="var(--line-2)">
<!-- divider -->
<line x1="560" y1="0" x2="560" y2="452" stroke="var(--line)" stroke-width="1" stroke-dasharray="3 4"/>
<!-- LEFT -->
<text x="0" y="22" font-family="'JetBrains Mono', monospace" font-size="11" font-weight="600" letter-spacing="1.1" fill="var(--accent-ink)">TODAY · ASSIGNED JOB</text>
<text x="0" y="44" font-size="13.5" fill="var(--ink-2)">Each parcel scored on its own. The rider set is</text>
<text x="0" y="62" font-size="13.5" fill="var(--ink-2)">re-drawn from scratch every single time.</text>
<rect x="0" y="104" width="74" height="34" rx="3" fill="var(--surface-2)" stroke="var(--line-2)"/><text x="37" y="126" text-anchor="middle" font-size="11.5" fill="var(--ink)">P1</text>
<rect x="0" y="154" width="74" height="34" rx="3" fill="var(--surface-2)" stroke="var(--line-2)"/><text x="37" y="176" text-anchor="middle" font-size="11.5" fill="var(--ink)">P2</text>
<rect x="0" y="204" width="74" height="34" rx="3" fill="var(--surface-2)" stroke="var(--line-2)"/><text x="37" y="226" text-anchor="middle" font-size="11.5" fill="var(--ink)">P3</text>
<rect x="0" y="254" width="74" height="34" rx="3" fill="var(--surface-2)" stroke="var(--line-2)"/><text x="37" y="276" text-anchor="middle" font-size="11.5" fill="var(--ink)">P4</text>
<rect x="152" y="104" width="128" height="184" rx="3" fill="var(--accent-soft)" stroke="var(--accent)"/>
<text x="216" y="182" text-anchor="middle" font-size="12" font-weight="600" fill="var(--accent-ink)">GEOSEARCH</text>
<text x="216" y="200" text-anchor="middle" font-size="12" font-weight="600" fill="var(--accent-ink)">+ scoring</text>
<line x1="78" y1="121" x2="148" y2="150" stroke="currentColor" stroke-width="1.2" marker-end="url(#I_ar2)"/>
<line x1="78" y1="171" x2="148" y2="180" stroke="currentColor" stroke-width="1.2" marker-end="url(#I_ar2)"/>
<line x1="78" y1="221" x2="148" y2="212" stroke="currentColor" stroke-width="1.2" marker-end="url(#I_ar2)"/>
<line x1="78" y1="271" x2="148" y2="242" stroke="currentColor" stroke-width="1.2" marker-end="url(#I_ar2)"/>
<circle cx="430" cy="121" r="19" fill="var(--surface)" stroke="var(--line-2)"/><text x="430" y="126" text-anchor="middle" font-size="11" fill="var(--ink)">R1</text>
<circle cx="430" cy="171" r="19" fill="var(--surface)" stroke="var(--line-2)"/><text x="430" y="176" text-anchor="middle" font-size="11" fill="var(--ink)">R2</text>
<circle cx="430" cy="221" r="19" fill="var(--surface)" stroke="var(--line-2)"/><text x="430" y="226" text-anchor="middle" font-size="11" fill="var(--ink)">R3</text>
<circle cx="430" cy="271" r="19" fill="var(--surface)" stroke="var(--line-2)"/><text x="430" y="276" text-anchor="middle" font-size="11" fill="var(--ink)">R4</text>
<line x1="284" y1="150" x2="407" y2="221" stroke="var(--accent)" stroke-width="1.3" marker-end="url(#I_ar2)" color="var(--accent)"/>
<line x1="284" y1="180" x2="407" y2="121" stroke="var(--accent)" stroke-width="1.3" marker-end="url(#I_ar2)" color="var(--accent)"/>
<line x1="284" y1="212" x2="407" y2="271" stroke="var(--accent)" stroke-width="1.3" marker-end="url(#I_ar2)" color="var(--accent)"/>
<line x1="284" y1="242" x2="407" y2="171" stroke="var(--accent)" stroke-width="1.3" marker-end="url(#I_ar2)" color="var(--accent)"/>
<text x="0" y="336" font-size="12.5" fill="var(--muted)">No Route, Beat or Round entity exists in</text>
<text x="0" y="354" font-size="12.5" fill="var(--muted)">doormile_backend — only Tripsheet, which is</text>
<text x="0" y="372" font-size="12.5" fill="var(--muted)">a hub-to-hub manifest, not a delivery beat.</text>
<text x="0" y="412" font-family="'JetBrains Mono', monospace" font-size="11" fill="var(--accent-ink)">stops per rider: unstable, day to day</text>
<!-- RIGHT -->
<text x="600" y="22" font-family="'JetBrains Mono', monospace" font-size="11" font-weight="600" letter-spacing="1.1" fill="var(--alt)">MILK ROUND · OWNED BEAT</text>
<text x="600" y="44" font-size="13.5" fill="var(--ink-2)">One rider, one ordered sequence, unchanged</text>
<text x="600" y="62" font-size="13.5" fill="var(--ink-2)">tomorrow. Familiarity is the cost saving.</text>
<rect x="600" y="168" width="104" height="56" rx="3" fill="var(--alt-soft)" stroke="var(--alt)"/>
<text x="652" y="194" text-anchor="middle" font-size="12.5" font-weight="600" fill="var(--alt)">Rider A</text>
<text x="652" y="211" text-anchor="middle" font-size="10" fill="var(--alt)">owns beat 7</text>
<path d="M708,196 H752" fill="none" stroke="var(--alt)" stroke-width="1.5" marker-end="url(#I_ar2t)"/>
<path d="M782,196 H842 M872,196 H932 M962,196 H1022" fill="none" stroke="var(--alt)" stroke-width="1.5" marker-end="url(#I_ar2t)"/>
<circle cx="767" cy="196" r="19" fill="var(--alt-soft)" stroke="var(--alt)"/><text x="767" y="201" text-anchor="middle" font-size="11.5" font-weight="600" fill="var(--alt)">1</text>
<circle cx="857" cy="196" r="19" fill="var(--alt-soft)" stroke="var(--alt)"/><text x="857" y="201" text-anchor="middle" font-size="11.5" font-weight="600" fill="var(--alt)">2</text>
<circle cx="947" cy="196" r="19" fill="var(--alt-soft)" stroke="var(--alt)"/><text x="947" y="201" text-anchor="middle" font-size="11.5" font-weight="600" fill="var(--alt)">3</text>
<circle cx="1037" cy="196" r="19" fill="var(--alt-soft)" stroke="var(--alt)"/><text x="1037" y="201" text-anchor="middle" font-size="11.5" font-weight="600" fill="var(--alt)">4</text>
<path d="M1037,219 V262 H767 V219" fill="none" stroke="var(--alt)" stroke-width="1.3" stroke-dasharray="5 4" marker-end="url(#I_ar2t)"/>
<rect x="826" y="250" width="152" height="24" rx="3" fill="var(--surface)" stroke="var(--alt)"/>
<text x="902" y="267" text-anchor="middle" font-size="10.5" fill="var(--alt)">repeats tomorrow</text>
<rect x="600" y="316" width="474" height="62" rx="3" fill="var(--surface-2)" stroke="var(--line-2)"/>
<text x="618" y="340" font-size="12.5" font-weight="600" fill="var(--ink)">rider_substitutions</text>
<text x="618" y="360" font-size="11.5" fill="var(--muted)">absent rider → sub rider, per tenant, per date</text>
<text x="1056" y="352" text-anchor="end" font-size="10.5" font-family="'JetBrains Mono', monospace" fill="var(--alt)">jupiter</text>
<text x="600" y="412" font-family="'JetBrains Mono', monospace" font-size="11" fill="var(--alt)">stops per rider: stable, learned, dense</text>
</g>
</svg>
</div>
<figcaption><b>Figure I.2 — the seam</b>The right-hand pattern only needs a substitution table because a round has an owner. Doormile has no owner and no round; <code>backend_jupiter</code> has both. Two lineages, one repo.</figcaption>
</figure>
<div class="callout">
<span class="label">The tell</span>
<p><code>rider_substitutions</code> — absent rider, substitute rider, per tenant, per date, with a scheduled → active → completed lifecycle. On-demand dispatch has no concept of an absent rider; you simply assign someone else. That table can only exist if a specific person owns a specific round.</p>
</div>
<div class="table-scroll">
<table>
<thead>
<tr><th>Milk-round property</th><th>In Doormile today</th><th>Where it lives</th></tr>
</thead>
<tbody>
<tr><td>Promised time slot</td><td class="yes">Present</td><td><code>Preferredpickupfrom</code> / <code>Preferredpickupto</code> on <code>PickupBooking</code></td></tr>
<tr><td>Recurring account shape</td><td class="yes">Present</td><td>CRM captures <code>shipping_frequency</code>, <code>parcel_volume</code>, <code>active_contracts</code></td></tr>
<tr><td>Depot / dairy origin</td><td class="yes">Present</td><td><code>Tenant</code> → <code>TenantLocation</code>, per-site reporting on <code>Tenantlocationid</code></td></tr>
<tr><td>Dense local drop</td><td class="yes">Present</td><td>3-digit pincode prefix match skips the hub entirely</td></tr>
<tr><td>Day's order book in one drop</td><td class="yes">Present</td><td><code>POST /admin/expressbooking/bulk</code></td></tr>
<tr><td>Round ownership</td><td class="no">Absent</td><td>only <code>rider_substitutions</code>, and only in <code>backend_jupiter</code></td></tr>
<tr><td>Beat with ordered stops</td><td class="no">Absent</td><td>no Route / Beat / Round model — <code>Tripsheet</code> is linehaul only</td></tr>
<tr><td>Standing orders</td><td class="no">Absent</td><td>the only "subscription" in the backend is NATS message subscriptions</td></tr>
<tr><td>Waves as delivery rounds</td><td class="no">Absent</td><td>frontend display filter in <code>utils/batchBucket.js</code> — see below</td></tr>
</tbody>
</table>
</div>
</section>
<section id="i-waves">
<div class="sec-head">
<span class="sec-num">03</span>
<div><h2>Why a wave isn't a round</h2></div>
</div>
<p class="measure">Morning / Afternoon / Evening look like rounds. They aren't. They bucket on <code>orderdate</code> — when the order was <em>placed</em> — so a wave describes arrival, not departure. The dispatch folder's own <code>CLAUDE.md</code> records that the promised-delivery field was tried here and deliberately rejected.</p>
<figure>
<div class="fig-scroll">
<svg viewBox="0 0 1120 330" role="img" aria-label="A 24-hour timeline split into morning, afternoon and evening bands. One parcel placed at 12:22 with a promised delivery of 16:30 falls in the Afternoon bucket when bucketed on order date, but in the Evening bucket when bucketed on expected delivery time.">
<defs>
<marker id="I_ar3" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="6" markerHeight="6" orient="auto-start-reverse">
<path d="M0,0 L10,5 L0,10 z" fill="currentColor"/>
</marker>
</defs>
<g font-family="'Source Sans 3', sans-serif" color="var(--line-2)">
<!-- bands: x = 60 + hour*42 (24h -> 1008px) -->
<rect x="60" y="56" width="378" height="52" fill="var(--surface-2)" stroke="var(--line-2)"/>
<rect x="438" y="56" width="294" height="52" fill="var(--surface-2)" stroke="var(--line-2)"/>
<rect x="732" y="56" width="336" height="52" fill="var(--surface-2)" stroke="var(--line-2)"/>
<text x="249" y="87" text-anchor="middle" font-size="12.5" font-weight="600" fill="var(--ink)">Morning · 0–9</text>
<text x="585" y="87" text-anchor="middle" font-size="12.5" font-weight="600" fill="var(--ink)">Afternoon · 9–16</text>
<text x="900" y="87" text-anchor="middle" font-size="12.5" font-weight="600" fill="var(--ink)">Evening · 16–24</text>
<text x="60" y="42" text-anchor="middle" font-size="10.5" font-family="'JetBrains Mono', monospace" fill="var(--muted)">12 AM</text>
<text x="438" y="42" text-anchor="middle" font-size="10.5" font-family="'JetBrains Mono', monospace" fill="var(--muted)">9 AM</text>
<text x="732" y="42" text-anchor="middle" font-size="10.5" font-family="'JetBrains Mono', monospace" fill="var(--muted)">4 PM</text>
<text x="1068" y="42" text-anchor="middle" font-size="10.5" font-family="'JetBrains Mono', monospace" fill="var(--muted)">12 AM</text>
<!-- row A: orderdate -->
<text x="0" y="168" font-size="12.5" font-weight="600" fill="var(--accent-ink)">bucket on orderdate</text>
<text x="0" y="185" font-size="11" fill="var(--muted)">current</text>
<line x1="60" y1="176" x2="1068" y2="176" stroke="var(--line)" stroke-width="1"/>
<circle cx="578" cy="176" r="7" fill="var(--accent)"/>
<text x="578" y="207" text-anchor="middle" font-size="11.5" font-family="'JetBrains Mono', monospace" fill="var(--accent-ink)">placed 12:22</text>
<line x1="578" y1="112" x2="578" y2="166" stroke="var(--accent)" stroke-width="1.3" stroke-dasharray="4 3"/>
<text x="600" y="132" font-size="11.5" font-weight="600" fill="var(--accent-ink)">→ Afternoon</text>
<!-- row B: expecteddeliverytime -->
<text x="0" y="264" font-size="12.5" font-weight="600" fill="var(--alt)">bucket on expecteddeliverytime</text>
<text x="0" y="281" font-size="11" fill="var(--muted)">jupiter · rejected here</text>
<line x1="60" y1="272" x2="1068" y2="272" stroke="var(--line)" stroke-width="1"/>
<circle cx="753" cy="272" r="7" fill="var(--alt)"/>
<text x="753" y="303" text-anchor="middle" font-size="11.5" font-family="'JetBrains Mono', monospace" fill="var(--alt)">promised 16:30</text>
<path d="M753,262 V232 H820" fill="none" stroke="var(--alt)" stroke-width="1.3" stroke-dasharray="4 3" marker-end="url(#I_ar3)" color="var(--alt)"/>
<text x="828" y="236" font-size="11.5" font-weight="600" fill="var(--alt)">→ Evening</text>
<line x1="578" y1="176" x2="753" y2="272" stroke="var(--muted)" stroke-width="1" stroke-dasharray="2 4"/>
</g>
</svg>
</div>
<figcaption><b>Figure I.3 — the same parcel, two waves</b>Only the lower one describes when the parcel goes out. Bucketing on arrival is correct for a courier queue and wrong for a round — which is why the windows inherited from jupiter had to be widened to cover all 24 hours once the field changed.</figcaption>
</figure>
<p class="measure">There is a second constraint recorded in that file worth carrying forward: the filter field and the bucket field must be the same field. An earlier attempt to admit rows on <code>updatedat</code> while bucketing on <code>createdat</code> put six orders in the Evening batch at 10:41 in the morning on a day with no orders at all.</p>
</section>
<section id="i-native">
<div class="sec-head">
<span class="sec-num">04</span>
<div><h2>What a native milk round would need</h2></div>
</div>
<p class="measure">Four additions, roughly in dependency order. None of them fight the existing pipeline — the consignment, OTP and proof-of-delivery machinery downstream of stage 05 stays exactly as it is.</p>
<ol class="stages">
<li><span class="n">01</span><span><b>A <code>Beat</code> entity</b> <span class="d">— an ordered stop sequence owned by a tenant and a hub, with a slot window. This is the object the whole model is missing.</span></span></li>
<li><span class="n">02</span><span><b>Rider-to-beat assignment that persists</b> <span class="d">— across days, not per parcel. <code>rider_substitutions</code> in jupiter is already the covering mechanism; it just needs a beat to point at.</span></span></li>
<li><span class="n">03</span><span><b>A standing-order generator</b> <span class="d">— materialises tomorrow's bookings from a recurrence rule against a <code>TenantLocation</code>, so the day's round exists before anyone books it.</span></span></li>
<li><span class="n">04</span><span><b>Bucketing moved to the slot field</b> <span class="d">— at which point the original clustered windows become correct again, and a wave finally means a departure.</span></span></li>
</ol>
<div class="callout">
<span class="label">What the AI layer becomes</span>
<p>Dispatch stops choosing a rider per parcel and starts doing two different jobs: <b>sequencing</b> a beat's stops before it goes out, and handling <b>exceptions</b> — the overflow parcel, the absent rider, the address that doesn't fit today's round. That is a smaller, sharper problem than scoring every candidate on every booking, and it preserves the density that makes the model pay.</p>
</div>
</section>
<div class="part">
<p class="part-kicker">Part II</p>
<h2 class="part-title">Doormile Route Intelligence</h2>
<p class="part-intro">Each leg of the journey drawn on its own — <strong>first</strong>, <strong>mid</strong>, <strong>last</strong> — with its real states, endpoints and failure branches. Then all three as one chain. Then where an AI-optimised design would actually put its intelligence, which is not where Doormile puts it now.</p>
</div>
<section id="first">
<div class="sec-head"><span class="sec-num">01</span><span class="tag t-first">First mile</span><div><h2>Shipper to origin hub</h2></div></div>
<p class="measure">Everything from a booking existing to a parcel being in a rider's hands. The happy path runs down the centre; every exception branch on the right is one the backend actually implements.</p>
<figure>
<div class="fig-scroll">
<svg viewBox="0 0 1120 960" role="img" aria-label="First mile flow. A booking is created and queued; geo-search finds candidate milers or escalates when none are in radius; the AI layer scores them within five seconds or falls back to greedy nearest; the assignment commits in a transaction and a push notification goes out; the rider accepts or rejects, a rejection reassigning back to the search; on accept the rider proceeds to pickup scheduled, arrived at pickup, and pickup complete, which converts the booking into a consignment collected by miler.">
<defs>
<marker id="II_f1" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse"><path d="M0,0 L10,5 L0,10 z" fill="currentColor"/></marker>
<marker id="II_f1a" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse"><path d="M0,0 L10,5 L0,10 z" fill="var(--accent)"/></marker>
</defs>
<g font-family="'Source Sans 3', sans-serif" color="var(--line-2)">
<text x="300" y="20" text-anchor="middle" font-family="'JetBrains Mono', monospace" font-size="10.5" letter-spacing=".1em" fill="var(--muted)">HAPPY PATH</text>
<text x="880" y="20" text-anchor="middle" font-family="'JetBrains Mono', monospace" font-size="10.5" letter-spacing=".1em" fill="var(--accent-ink)">EXCEPTIONS</text>
<line x1="660" y1="34" x2="660" y2="930" stroke="var(--line)" stroke-width="1" stroke-dasharray="3 4"/>
<!-- 1 -->
<rect x="140" y="40" width="320" height="58" rx="3" fill="var(--surface-2)" stroke="var(--ink)" stroke-width="1.5"/>
<text x="300" y="64" text-anchor="middle" font-size="13" font-weight="600" fill="var(--ink)">Booking created</text>
<text x="300" y="83" text-anchor="middle" font-size="10.5" font-family="'JetBrains Mono', monospace" fill="var(--muted)">Created | Pending_Pickup</text>
<line x1="300" y1="98" x2="300" y2="126" stroke="currentColor" stroke-width="1.4" marker-end="url(#II_f1)"/>
<!-- 2 -->
<rect x="140" y="128" width="320" height="58" rx="3" fill="var(--surface-2)" stroke="var(--line-2)"/>
<text x="300" y="152" text-anchor="middle" font-size="13" font-weight="600" fill="var(--ink)">NATS queue → TryAssignOnce</text>
<text x="300" y="171" text-anchor="middle" font-size="10.5" font-family="'JetBrains Mono', monospace" fill="var(--muted)">60 s worker budget</text>
<line x1="300" y1="186" x2="300" y2="214" stroke="currentColor" stroke-width="1.4" marker-end="url(#II_f1)"/>
<!-- 3 decision -->
<rect x="140" y="216" width="320" height="58" rx="29" fill="var(--surface)" stroke="var(--ink)" stroke-width="1.5"/>
<text x="300" y="240" text-anchor="middle" font-size="13" font-weight="600" fill="var(--ink)">GEOSEARCH — any candidates?</text>
<text x="300" y="259" text-anchor="middle" font-size="10.5" font-family="'JetBrains Mono', monospace" fill="var(--muted)">geoRadiusKm 10 · geoMaxCount 10</text>
<path d="M460,245 H700" fill="none" stroke="var(--accent)" stroke-width="1.4" marker-end="url(#II_f1a)"/>
<text x="580" y="238" text-anchor="middle" font-size="10.5" font-weight="600" fill="var(--accent-ink)">none in radius</text>
<rect x="704" y="218" width="372" height="54" rx="3" fill="var(--accent-soft)" stroke="var(--accent)"/>
<text x="890" y="240" text-anchor="middle" font-size="12" font-weight="600" fill="var(--accent-ink)">Escalated → manual assignment</text>
<text x="890" y="258" text-anchor="middle" font-size="10.5" font-family="'JetBrains Mono', monospace" fill="var(--accent-ink)">/admin/bookings/:id/assign-miler</text>
<line x1="300" y1="274" x2="300" y2="302" stroke="currentColor" stroke-width="1.4" marker-end="url(#II_f1)"/>
<!-- 4 -->
<rect x="140" y="304" width="320" height="58" rx="3" fill="var(--accent-soft)" stroke="var(--accent)"/>
<text x="300" y="328" text-anchor="middle" font-size="13" font-weight="600" fill="var(--accent-ink)">AI layer scores candidates</text>
<text x="300" y="347" text-anchor="middle" font-size="10.5" font-family="'JetBrains Mono', monospace" fill="var(--accent-ink)">routemate.workolik.com · 5 s cap</text>
<path d="M460,333 H700" fill="none" stroke="var(--accent)" stroke-width="1.4" stroke-dasharray="5 4" marker-end="url(#II_f1a)"/>
<text x="580" y="326" text-anchor="middle" font-size="10.5" font-weight="600" fill="var(--accent-ink)">timeout</text>
<rect x="704" y="306" width="372" height="54" rx="3" fill="var(--surface-2)" stroke="var(--line-2)"/>
<text x="890" y="328" text-anchor="middle" font-size="12" font-weight="600" fill="var(--ink)">Greedy nearest miler</text>
<text x="890" y="346" text-anchor="middle" font-size="10.5" fill="var(--muted)">deterministic fallback, never blocks</text>
<line x1="300" y1="362" x2="300" y2="390" stroke="currentColor" stroke-width="1.4" marker-end="url(#II_f1)"/>
<!-- 5 -->
<rect x="140" y="392" width="320" height="76" rx="3" fill="var(--surface-2)" stroke="var(--ink)" stroke-width="1.5"/>
<text x="300" y="416" text-anchor="middle" font-size="13" font-weight="600" fill="var(--ink)">commitAssignment — one transaction</text>
<text x="300" y="435" text-anchor="middle" font-size="10.5" font-family="'JetBrains Mono', monospace" fill="var(--muted)">booking → Miler_Assigned</text>
<text x="300" y="451" text-anchor="middle" font-size="10.5" font-family="'JetBrains Mono', monospace" fill="var(--muted)">assignment → Assigned · miler → Assigned</text>
<path d="M460,430 H700" fill="none" stroke="var(--muted)" stroke-width="1.2" stroke-dasharray="4 3" marker-end="url(#II_f1)"/>
<rect x="704" y="404" width="372" height="52" rx="3" fill="var(--surface)" stroke="var(--line-2)"/>
<text x="890" y="426" text-anchor="middle" font-size="12" fill="var(--ink)">AgentDecision row written</text>
<text x="890" y="444" text-anchor="middle" font-size="10.5" fill="var(--muted)">reasoning + candidate count, for audit</text>
<line x1="300" y1="468" x2="300" y2="496" stroke="currentColor" stroke-width="1.4" marker-end="url(#II_f1)"/>
<!-- 6 -->
<rect x="140" y="498" width="320" height="52" rx="3" fill="var(--surface-2)" stroke="var(--line-2)"/>
<text x="300" y="529" text-anchor="middle" font-size="13" fill="var(--ink)">push notification to rider</text>
<line x1="300" y1="550" x2="300" y2="578" stroke="currentColor" stroke-width="1.4" marker-end="url(#II_f1)"/>
<!-- 7 decision -->
<rect x="140" y="580" width="320" height="52" rx="26" fill="var(--surface)" stroke="var(--ink)" stroke-width="1.5"/>
<text x="300" y="611" text-anchor="middle" font-size="13" font-weight="600" fill="var(--ink)">rider accepts?</text>
<path d="M460,606 H700" fill="none" stroke="var(--accent)" stroke-width="1.4" marker-end="url(#II_f1a)"/>
<text x="580" y="599" text-anchor="middle" font-size="10.5" font-weight="600" fill="var(--accent-ink)">reject</text>
<rect x="704" y="580" width="372" height="52" rx="3" fill="var(--accent-soft)" stroke="var(--accent)"/>
<text x="890" y="602" text-anchor="middle" font-size="12" font-weight="600" fill="var(--accent-ink)">Rejected → Reassigned</text>
<text x="890" y="620" text-anchor="middle" font-size="10.5" fill="var(--accent-ink)">back into the search</text>
<path d="M1076,606 H1100 V245 H1080" fill="none" stroke="var(--accent)" stroke-width="1.3" stroke-dasharray="5 4" marker-end="url(#II_f1a)"/>
<line x1="300" y1="632" x2="300" y2="660" stroke="currentColor" stroke-width="1.4" marker-end="url(#II_f1)"/>
<!-- 8 -->
<rect x="140" y="662" width="320" height="58" rx="3" fill="var(--surface-2)" stroke="var(--line-2)"/>
<text x="300" y="686" text-anchor="middle" font-size="13" font-weight="600" fill="var(--ink)">Accept</text>
<text x="300" y="705" text-anchor="middle" font-size="10.5" font-family="'JetBrains Mono', monospace" fill="var(--muted)">Pickup_Scheduled · miler On_Pickup</text>
<line x1="300" y1="720" x2="300" y2="748" stroke="currentColor" stroke-width="1.4" marker-end="url(#II_f1)"/>
<!-- 9 -->
<rect x="140" y="750" width="320" height="58" rx="3" fill="var(--surface-2)" stroke="var(--line-2)"/>
<text x="300" y="774" text-anchor="middle" font-size="13" font-weight="600" fill="var(--ink)">Reached doorstep</text>
<text x="300" y="793" text-anchor="middle" font-size="10.5" font-family="'JetBrains Mono', monospace" fill="var(--muted)">Arrived_At_Pickup · arrivedat + GPS</text>
<line x1="300" y1="808" x2="300" y2="836" stroke="currentColor" stroke-width="1.4" marker-end="url(#II_f1)"/>
<!-- 10 -->
<rect x="140" y="838" width="320" height="76" rx="3" fill="var(--surface-2)" stroke="var(--ink)" stroke-width="2"/>
<text x="300" y="862" text-anchor="middle" font-size="13" font-weight="600" fill="var(--ink)">Pickup complete</text>
<text x="300" y="881" text-anchor="middle" font-size="10.5" font-family="'JetBrains Mono', monospace" fill="var(--muted)">Picked_Up → Converted_To_Consignment</text>
<text x="300" y="897" text-anchor="middle" font-size="10.5" font-family="'JetBrains Mono', monospace" fill="var(--muted)">COD collected · Idempotency-Key</text>
<path d="M460,876 H700" fill="none" stroke="var(--muted)" stroke-width="1.2" stroke-dasharray="4 3" marker-end="url(#II_f1)"/>
<rect x="704" y="838" width="372" height="76" rx="3" fill="var(--surface)" stroke="var(--line-2)"/>
<text x="890" y="862" text-anchor="middle" font-size="12" font-weight="600" fill="var(--ink)">Consignment issued</text>
<text x="890" y="881" text-anchor="middle" font-size="10.5" font-family="'JetBrains Mono', monospace" fill="var(--muted)">Collected_By_Miler · trackingno</text>
<text x="890" y="897" text-anchor="middle" font-size="10.5" font-family="'JetBrains Mono', monospace" fill="var(--muted)">+ 6-digit OTP to receiver</text>
</g>
</svg>
</div>
<figcaption><b>Figure II.1 — first mile</b>Every branch here is real: escalation on an empty radius, the five-second greedy fallback, rejection looping back into the search, and the idempotency key that stops a retried <code>pickup-complete</code> collecting COD twice.</figcaption>
</figure>
<p class="measure">Two details worth holding onto. <code>ALREADY_PICKED_UP</code> and <code>INVALID_STATE</code> are stable machine-readable codes the rider app branches on, so the state machine is enforced server-side rather than trusted from the client. And <code>commitAssignment</code> writes booking, assignment and miler availability in a single transaction — there is no window where a rider is holding a job the booking does not know about.</p>
</section>
<section id="mid">
<div class="sec-head"><span class="sec-num">02</span><span class="tag t-mid">Mid mile</span><div><h2>Hub to hub</h2></div></div>
<p class="measure">The line-haul leg. This is the only one with no automation at all — every decision below is made by a person at a screen.</p>
<figure>
<div class="fig-scroll">
<svg viewBox="0 0 1120 900" role="img" aria-label="Mid mile flow. The consignment is inbound-scanned at the origin hub with a condition recorded and a shelf assigned, becoming inwarded at hub; damaged condition diverts to a hold. If the destination hub equals the current hub the parcel skips straight to last mile. Otherwise a tripsheet is created as draft, items are scanned and loaded with discrepancies flagged, a vehicle and driver are attached to make it ready, dispatch puts it in transit, and arrival unloads it. If the destination is still not reached the parcel is re-inwarded and joins the next tripsheet.">
<defs>
<marker id="II_f2" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse"><path d="M0,0 L10,5 L0,10 z" fill="currentColor"/></marker>
<marker id="II_f2g" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse"><path d="M0,0 L10,5 L0,10 z" fill="var(--gold)"/></marker>
<marker id="II_f2t" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse"><path d="M0,0 L10,5 L0,10 z" fill="var(--alt)"/></marker>
</defs>
<g font-family="'Source Sans 3', sans-serif" color="var(--line-2)">
<text x="300" y="20" text-anchor="middle" font-family="'JetBrains Mono', monospace" font-size="10.5" letter-spacing=".1em" fill="var(--muted)">LINE-HAUL PATH</text>
<text x="880" y="20" text-anchor="middle" font-family="'JetBrains Mono', monospace" font-size="10.5" letter-spacing=".1em" fill="var(--gold)">BRANCHES</text>
<line x1="660" y1="34" x2="660" y2="870" stroke="var(--line)" stroke-width="1" stroke-dasharray="3 4"/>
<!-- 1 -->
<rect x="140" y="40" width="320" height="52" rx="3" fill="var(--surface-2)" stroke="var(--ink)" stroke-width="1.5"/>
<text x="300" y="71" text-anchor="middle" font-size="13" font-weight="600" fill="var(--ink)">Parcel arrives at origin hub</text>
<line x1="300" y1="92" x2="300" y2="120" stroke="currentColor" stroke-width="1.4" marker-end="url(#II_f2)"/>
<!-- 2 -->
<rect x="140" y="122" width="320" height="76" rx="3" fill="var(--surface-2)" stroke="var(--line-2)"/>
<text x="300" y="146" text-anchor="middle" font-size="13" font-weight="600" fill="var(--ink)">Inbound barcode scan</text>
<text x="300" y="165" text-anchor="middle" font-size="10.5" font-family="'JetBrains Mono', monospace" fill="var(--muted)">shelf assigned · Condition recorded</text>
<text x="300" y="181" text-anchor="middle" font-size="10.5" font-family="'JetBrains Mono', monospace" fill="var(--muted)">→ Inwarded_at_Hub</text>
<path d="M460,160 H700" fill="none" stroke="var(--gold)" stroke-width="1.4" marker-end="url(#II_f2g)"/>
<text x="580" y="153" text-anchor="middle" font-size="10.5" font-weight="600" fill="var(--gold)">Condition ≠ Good</text>
<rect x="704" y="134" width="372" height="52" rx="3" fill="var(--gold-soft)" stroke="var(--gold)"/>
<text x="890" y="156" text-anchor="middle" font-size="12" font-weight="600" fill="var(--gold)">Damaged — held at hub</text>
<text x="890" y="174" text-anchor="middle" font-size="10.5" fill="var(--gold)">exception queue, not loaded</text>
<line x1="300" y1="198" x2="300" y2="226" stroke="currentColor" stroke-width="1.4" marker-end="url(#II_f2)"/>
<!-- 3 decision -->
<rect x="140" y="228" width="320" height="58" rx="29" fill="var(--surface)" stroke="var(--ink)" stroke-width="1.5"/>
<text x="300" y="252" text-anchor="middle" font-size="13" font-weight="600" fill="var(--ink)">destination hub = this hub?</text>
<text x="300" y="271" text-anchor="middle" font-size="10.5" font-family="'JetBrains Mono', monospace" fill="var(--muted)">currenthubid vs destinationhubid</text>
<path d="M460,257 H700" fill="none" stroke="var(--alt)" stroke-width="1.4" marker-end="url(#II_f2t)"/>
<text x="580" y="250" text-anchor="middle" font-size="10.5" font-weight="600" fill="var(--alt)">yes</text>
<rect x="704" y="230" width="372" height="54" rx="3" fill="var(--alt-soft)" stroke="var(--alt)"/>
<text x="890" y="252" text-anchor="middle" font-size="12" font-weight="600" fill="var(--alt)">mid mile skipped</text>
<text x="890" y="270" text-anchor="middle" font-size="10.5" fill="var(--alt)">straight to last-mile assignment</text>
<line x1="300" y1="286" x2="300" y2="314" stroke="currentColor" stroke-width="1.4" marker-end="url(#II_f2)"/>
<text x="316" y="304" font-size="10.5" font-weight="600" fill="var(--muted)">no</text>
<!-- 4 -->
<rect x="140" y="316" width="320" height="76" rx="3" fill="var(--gold-soft)" stroke="var(--gold)"/>
<text x="300" y="340" text-anchor="middle" font-size="13" font-weight="600" fill="var(--gold)">Tripsheet created by hand</text>
<text x="300" y="359" text-anchor="middle" font-size="10.5" font-family="'JetBrains Mono', monospace" fill="var(--gold)">status Draft · source + destination hub</text>
<text x="300" y="375" text-anchor="middle" font-size="10.5" font-family="'JetBrains Mono', monospace" fill="var(--gold)">Batchkind: local | transfer</text>
<line x1="300" y1="392" x2="300" y2="420" stroke="currentColor" stroke-width="1.4" marker-end="url(#II_f2)"/>
<!-- 5 -->
<rect x="140" y="422" width="320" height="76" rx="3" fill="var(--surface-2)" stroke="var(--line-2)"/>
<text x="300" y="446" text-anchor="middle" font-size="13" font-weight="600" fill="var(--ink)">Items scanned onto the sheet</text>
<text x="300" y="465" text-anchor="middle" font-size="10.5" font-family="'JetBrains Mono', monospace" fill="var(--muted)">item Pending → Loaded</text>
<text x="300" y="481" text-anchor="middle" font-size="10.5" font-family="'JetBrains Mono', monospace" fill="var(--muted)">consignment → Tripsheet_Loaded</text>
<path d="M460,460 H700" fill="none" stroke="var(--gold)" stroke-width="1.4" marker-end="url(#II_f2g)"/>
<text x="580" y="453" text-anchor="middle" font-size="10.5" font-weight="600" fill="var(--gold)">mismatch</text>
<rect x="704" y="434" width="372" height="52" rx="3" fill="var(--gold-soft)" stroke="var(--gold)"/>
<text x="890" y="456" text-anchor="middle" font-size="12" font-weight="600" fill="var(--gold)">Discrepancy</text>
<text x="890" y="474" text-anchor="middle" font-size="10.5" fill="var(--gold)">scanned but not on the manifest, or missing</text>
<line x1="300" y1="498" x2="300" y2="526" stroke="currentColor" stroke-width="1.4" marker-end="url(#II_f2)"/>
<!-- 6 -->
<rect x="140" y="528" width="320" height="58" rx="3" fill="var(--surface-2)" stroke="var(--line-2)"/>
<text x="300" y="552" text-anchor="middle" font-size="13" font-weight="600" fill="var(--ink)">Vehicle + driver attached</text>
<text x="300" y="571" text-anchor="middle" font-size="10.5" font-family="'JetBrains Mono', monospace" fill="var(--muted)">status → Ready</text>
<line x1="300" y1="586" x2="300" y2="614" stroke="currentColor" stroke-width="1.4" marker-end="url(#II_f2)"/>
<!-- 7 -->
<rect x="140" y="616" width="320" height="58" rx="3" fill="var(--surface-2)" stroke="var(--ink)" stroke-width="1.5"/>
<text x="300" y="640" text-anchor="middle" font-size="13" font-weight="600" fill="var(--ink)">Dispatch</text>
<text x="300" y="659" text-anchor="middle" font-size="10.5" font-family="'JetBrains Mono', monospace" fill="var(--muted)">Dispatched · dispatchtime · → In_Transit</text>
<path d="M460,645 H700" fill="none" stroke="var(--gold)" stroke-width="1.4" stroke-dasharray="5 4" marker-end="url(#II_f2g)"/>
<rect x="704" y="618" width="372" height="54" rx="3" fill="var(--surface)" stroke="var(--gold)" stroke-dasharray="4 3"/>
<text x="890" y="640" text-anchor="middle" font-size="12" font-weight="600" fill="var(--gold)">hold-vs-go decided by guess</text>
<text x="890" y="658" text-anchor="middle" font-size="10.5" fill="var(--gold)">depart at 80% fill, or wait for 95%?</text>
<line x1="300" y1="674" x2="300" y2="702" stroke="currentColor" stroke-width="1.4" marker-end="url(#II_f2)"/>
<!-- 8 -->
<rect x="140" y="704" width="320" height="76" rx="3" fill="var(--surface-2)" stroke="var(--line-2)"/>
<text x="300" y="728" text-anchor="middle" font-size="13" font-weight="600" fill="var(--ink)">Arrival scan &amp; offload</text>
<text x="300" y="747" text-anchor="middle" font-size="10.5" font-family="'JetBrains Mono', monospace" fill="var(--muted)">Arrived · arrivaltime · items Unloaded</text>
<text x="300" y="763" text-anchor="middle" font-size="10.5" font-family="'JetBrains Mono', monospace" fill="var(--muted)">currenthubid updated</text>
<line x1="300" y1="780" x2="300" y2="808" stroke="currentColor" stroke-width="1.4" marker-end="url(#II_f2)"/>
<!-- 9 decision -->
<rect x="140" y="810" width="320" height="58" rx="29" fill="var(--surface)" stroke="var(--ink)" stroke-width="2"/>
<text x="300" y="834" text-anchor="middle" font-size="13" font-weight="600" fill="var(--ink)">final destination reached?</text>
<text x="300" y="853" text-anchor="middle" font-size="10.5" fill="var(--muted)">yes → last mile</text>
<path d="M140,839 H80 V353 H136" fill="none" stroke="var(--gold)" stroke-width="1.4" stroke-dasharray="5 4" marker-end="url(#II_f2g)"/>
<text x="66" y="600" font-size="10.5" font-weight="600" fill="var(--gold)" transform="rotate(-90 66 600)">no — re-inward, next leg</text>
</g>
</svg>
</div>
<figcaption><b>Figure II.2 — mid mile</b>Note the loop on the left: a parcel can ride more than one tripsheet. Nothing decides which parcels board which truck, when it should leave, or whether the lane should exist — those are the three open decisions on this leg.</figcaption>
</figure>
<div class="callout gold">
<span class="label">The unpriced decision</span>
<p>Dispatch timing is the central mid-mile trade: the marginal cost of running an extra vehicle against the SLA-breach risk of the parcels you hold back. Both sides are computable — you already store <code>sladueat</code> on every consignment. Today it is a dispatcher's instinct.</p>
</div>
</section>
<section id="last">
<div class="sec-head"><span class="sec-num">03</span><span class="tag t-last">Last mile</span><div><h2>Destination hub to consignee</h2></div></div>
<p class="measure">The only leg with real road-network routing — and the only one with a retry loop, because a delivery can fail in a way a pickup cannot.</p>
<figure>
<div class="fig-scroll">
<svg viewBox="0 0 1120 900" role="img" aria-label="Last mile flow. A rider is assigned by one of three routes: manual, synchronous auto-assign, or greedy batch assign. Stops are then sequenced against a Valhalla road network. The rider starts delivery, moving the consignment to out for delivery, reaches the consignee, and either verifies the six-digit OTP and files a delivery proof to reach delivered, or skips, incrementing the attempt count and returning to the queue. Once attempts are exhausted the consignment goes to RTO initiated and then returned to sender.">
<defs>
<marker id="II_f3" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse"><path d="M0,0 L10,5 L0,10 z" fill="currentColor"/></marker>
<marker id="II_f3a" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse"><path d="M0,0 L10,5 L0,10 z" fill="var(--accent)"/></marker>
<marker id="II_f3t" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse"><path d="M0,0 L10,5 L0,10 z" fill="var(--alt)"/></marker>
</defs>
<g font-family="'Source Sans 3', sans-serif" color="var(--line-2)">
<text x="300" y="20" text-anchor="middle" font-family="'JetBrains Mono', monospace" font-size="10.5" letter-spacing=".1em" fill="var(--muted)">DELIVERY PATH</text>
<text x="880" y="20" text-anchor="middle" font-family="'JetBrains Mono', monospace" font-size="10.5" letter-spacing=".1em" fill="var(--accent-ink)">FAILURE PATH</text>
<line x1="660" y1="34" x2="660" y2="870" stroke="var(--line)" stroke-width="1" stroke-dasharray="3 4"/>
<!-- 1: three assignment doors -->
<rect x="140" y="40" width="100" height="46" rx="3" fill="var(--surface)" stroke="var(--line-2)"/>
<text x="190" y="61" text-anchor="middle" font-size="11" fill="var(--ink)">manual</text>
<text x="190" y="76" text-anchor="middle" font-size="9.5" font-family="'JetBrains Mono', monospace" fill="var(--muted)">assign-miler</text>
<rect x="250" y="40" width="100" height="46" rx="3" fill="var(--surface)" stroke="var(--line-2)"/>
<text x="300" y="61" text-anchor="middle" font-size="11" fill="var(--ink)">sync AI</text>
<text x="300" y="76" text-anchor="middle" font-size="9.5" font-family="'JetBrains Mono', monospace" fill="var(--muted)">auto-assign</text>
<rect x="360" y="40" width="100" height="46" rx="3" fill="var(--surface)" stroke="var(--line-2)"/>
<text x="410" y="61" text-anchor="middle" font-size="11" fill="var(--ink)">greedy</text>
<text x="410" y="76" text-anchor="middle" font-size="9.5" font-family="'JetBrains Mono', monospace" fill="var(--muted)">batch-assign</text>
<path d="M190,86 V104 H300" fill="none" stroke="currentColor" stroke-width="1.3"/>
<path d="M410,86 V104 H300" fill="none" stroke="currentColor" stroke-width="1.3"/>
<line x1="300" y1="86" x2="300" y2="104" stroke="currentColor" stroke-width="1.3"/>
<line x1="300" y1="104" x2="300" y2="126" stroke="currentColor" stroke-width="1.4" marker-end="url(#II_f3)"/>
<!-- 2 -->
<rect x="140" y="128" width="320" height="58" rx="3" fill="var(--surface-2)" stroke="var(--ink)" stroke-width="1.5"/>
<text x="300" y="152" text-anchor="middle" font-size="13" font-weight="600" fill="var(--ink)">Rider assigned</text>
<text x="300" y="171" text-anchor="middle" font-size="10.5" font-family="'JetBrains Mono', monospace" fill="var(--muted)">assignment Assigned · miler On_Delivery</text>
<line x1="300" y1="186" x2="300" y2="214" stroke="currentColor" stroke-width="1.4" marker-end="url(#II_f3)"/>
<!-- 3 -->
<rect x="140" y="216" width="320" height="76" rx="3" fill="var(--alt-soft)" stroke="var(--alt)"/>
<text x="300" y="240" text-anchor="middle" font-size="13" font-weight="600" fill="var(--alt)">SequenceMilerStops → Valhalla</text>
<text x="300" y="259" text-anchor="middle" font-size="10.5" font-family="'JetBrains Mono', monospace" fill="var(--alt)">writes step · previouskms · cumulativeeta</text>
<text x="300" y="275" text-anchor="middle" font-size="10.5" fill="var(--alt)">async, best-effort, needs ≥ 2 stops</text>
<path d="M460,254 H700" fill="none" stroke="var(--alt)" stroke-width="1.3" stroke-dasharray="4 3" marker-end="url(#II_f3t)"/>
<rect x="704" y="228" width="372" height="52" rx="3" fill="var(--surface)" stroke="var(--alt)" stroke-dasharray="4 3"/>
<text x="890" y="250" text-anchor="middle" font-size="12" fill="var(--alt)">unconfigured → stops stay unsequenced</text>
<text x="890" y="268" text-anchor="middle" font-size="10.5" fill="var(--muted)">assignment never fails because of this</text>
<line x1="300" y1="292" x2="300" y2="320" stroke="currentColor" stroke-width="1.4" marker-end="url(#II_f3)"/>
<!-- 4 -->
<rect x="140" y="322" width="320" height="58" rx="3" fill="var(--surface-2)" stroke="var(--line-2)"/>
<text x="300" y="346" text-anchor="middle" font-size="13" font-weight="600" fill="var(--ink)">Start delivery</text>
<text x="300" y="365" text-anchor="middle" font-size="10.5" font-family="'JetBrains Mono', monospace" fill="var(--muted)">→ Out_for_Delivery</text>
<line x1="300" y1="380" x2="300" y2="408" stroke="currentColor" stroke-width="1.4" marker-end="url(#II_f3)"/>
<!-- 5 -->
<rect x="140" y="410" width="320" height="52" rx="3" fill="var(--surface-2)" stroke="var(--line-2)"/>
<text x="300" y="441" text-anchor="middle" font-size="13" fill="var(--ink)">rider reaches the consignee</text>
<line x1="300" y1="462" x2="300" y2="490" stroke="currentColor" stroke-width="1.4" marker-end="url(#II_f3)"/>
<!-- 6 decision -->
<rect x="140" y="492" width="320" height="58" rx="29" fill="var(--surface)" stroke="var(--ink)" stroke-width="1.5"/>
<text x="300" y="516" text-anchor="middle" font-size="13" font-weight="600" fill="var(--ink)">receiver present, OTP correct?</text>
<text x="300" y="535" text-anchor="middle" font-size="10.5" font-family="'JetBrains Mono', monospace" fill="var(--muted)">OTP_REQUIRED | OTP_INVALID</text>
<path d="M460,521 H700" fill="none" stroke="var(--accent)" stroke-width="1.4" marker-end="url(#II_f3a)"/>
<text x="580" y="514" text-anchor="middle" font-size="10.5" font-weight="600" fill="var(--accent-ink)">no</text>
<rect x="704" y="494" width="372" height="58" rx="3" fill="var(--accent-soft)" stroke="var(--accent)"/>
<text x="890" y="518" text-anchor="middle" font-size="12" font-weight="600" fill="var(--accent-ink)">Skip — failed attempt</text>
<text x="890" y="537" text-anchor="middle" font-size="10.5" font-family="'JetBrains Mono', monospace" fill="var(--accent-ink)">Attemptcount + 1 · reason logged</text>
<!-- retry loop -->
<path d="M1076,523 H1100 V152 H1080" fill="none" stroke="var(--accent)" stroke-width="1.3" stroke-dasharray="5 4" marker-end="url(#II_f3a)"/>
<text x="1114" y="330" font-size="10.5" font-weight="600" fill="var(--accent-ink)" transform="rotate(90 1114 330)">re-queue for next attempt</text>
<line x1="300" y1="550" x2="300" y2="578" stroke="currentColor" stroke-width="1.4" marker-end="url(#II_f3)"/>
<text x="316" y="570" font-size="10.5" font-weight="600" fill="var(--muted)">yes</text>
<!-- 7 -->
<rect x="140" y="580" width="320" height="94" rx="3" fill="var(--surface-2)" stroke="var(--ink)" stroke-width="1.5"/>
<text x="300" y="604" text-anchor="middle" font-size="13" font-weight="600" fill="var(--ink)">Deliver</text>
<text x="300" y="623" text-anchor="middle" font-size="10.5" font-family="'JetBrains Mono', monospace" fill="var(--muted)">OTP verified · Idempotency-Key</text>
<text x="300" y="639" text-anchor="middle" font-size="10.5" font-family="'JetBrains Mono', monospace" fill="var(--muted)">signature + photo via presigned URL</text>
<text x="300" y="659" text-anchor="middle" font-size="10.5" fill="var(--muted)">DeliveryProof row written</text>
<!-- attempts exhausted -->
<path d="M890,552 V596 H1000" fill="none" stroke="var(--accent)" stroke-width="1.4" marker-end="url(#II_f3a)"/>
<text x="778" y="578" font-size="10.5" font-weight="600" fill="var(--accent-ink)">attempts exhausted</text>
<rect x="704" y="612" width="372" height="52" rx="3" fill="var(--accent-soft)" stroke="var(--accent)"/>
<text x="890" y="634" text-anchor="middle" font-size="12" font-weight="600" fill="var(--accent-ink)">RTO_Initiated</text>
<text x="890" y="652" text-anchor="middle" font-size="10.5" font-family="'JetBrains Mono', monospace" fill="var(--accent-ink)">Returnreason · Parentconsignmentid</text>
<line x1="890" y1="664" x2="890" y2="692" stroke="var(--accent)" stroke-width="1.4" marker-end="url(#II_f3a)"/>
<rect x="704" y="694" width="372" height="52" rx="3" fill="var(--surface-2)" stroke="var(--accent)" stroke-width="1.5"/>
<text x="890" y="716" text-anchor="middle" font-size="12.5" font-weight="600" fill="var(--accent-ink)">Returned_to_Sender</text>
<text x="890" y="734" text-anchor="middle" font-size="10.5" fill="var(--muted)">the return leg is itself a full journey</text>
<line x1="300" y1="674" x2="300" y2="702" stroke="currentColor" stroke-width="1.4" marker-end="url(#II_f3)"/>
<!-- 8 -->
<rect x="140" y="704" width="320" height="76" rx="3" fill="var(--alt-soft)" stroke="var(--alt)" stroke-width="2"/>
<text x="300" y="730" text-anchor="middle" font-size="14" font-weight="600" fill="var(--alt)">Delivered</text>
<text x="300" y="750" text-anchor="middle" font-size="10.5" font-family="'JetBrains Mono', monospace" fill="var(--alt)">Codcollected reconciled</text>
<text x="300" y="766" text-anchor="middle" font-size="10.5" font-family="'JetBrains Mono', monospace" fill="var(--alt)">Billingstatus → Billed</text>
<!-- other terminals -->
<rect x="140" y="812" width="320" height="52" rx="3" fill="var(--surface)" stroke="var(--line-2)" stroke-dasharray="4 3"/>
<text x="300" y="834" text-anchor="middle" font-size="11.5" font-weight="600" fill="var(--muted)">other terminal states</text>
<text x="300" y="852" text-anchor="middle" font-size="10.5" font-family="'JetBrains Mono', monospace" fill="var(--muted)">Missing · Damaged · Cancelled</text>
</g>
</svg>
</div>
<figcaption><b>Figure II.3 — last mile</b><code>Attemptcount</code> and the skip reason are already being recorded on every failure. That is a labelled training set for first-attempt success prediction, sitting unused.</figcaption>
</figure>
<div class="callout teal">
<span class="label">Where the money leaks</span>
<p>Every trip around that retry loop is a second full delivery run for one parcel. First-attempt delivery rate is the dominant cost line in Indian last-mile, and the loop above is currently entered on discovery rather than predicted in advance.</p>
</div>
</section>
<section id="whole">
<div class="sec-head"><span class="sec-num">04</span><span class="tag t-all">All three</span><div><h2>The whole flow</h2></div></div>
<p class="measure">Every status a parcel can hold, in the order it holds them, colour-coded by leg. Read it as a snake: left to right, drop, right to left, drop, left to right.</p>
<figure>
<div class="fig-scroll">
<svg viewBox="0 0 1120 620" role="img" aria-label="The complete status chain as a snake. Row one, first mile, left to right: created or pending pickup, miler assigned, pickup scheduled, arrived at pickup, picked up, converted to consignment. Row two, mid mile, right to left: collected by miler, inwarded at hub, tripsheet loaded, in transit, with a loop back to inwarded at hub for multi-leg journeys. Row three, last mile, left to right: out for delivery then delivered. A hyperlocal bypass curve jumps from converted to consignment straight to out for delivery. Failure terminals branch off to RTO initiated and returned to sender, with missing, damaged and cancelled shown separately.">
<defs>
<marker id="II_w1" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse"><path d="M0,0 L10,5 L0,10 z" fill="currentColor"/></marker>
<marker id="II_w1t" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse"><path d="M0,0 L10,5 L0,10 z" fill="var(--alt)"/></marker>
<marker id="II_w1a" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse"><path d="M0,0 L10,5 L0,10 z" fill="var(--accent)"/></marker>
<marker id="II_w1g" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse"><path d="M0,0 L10,5 L0,10 z" fill="var(--gold)"/></marker>
</defs>
<g font-family="'JetBrains Mono', ui-monospace, monospace" color="var(--line-2)">
<!-- ROW 1 : first mile -->
<text x="20" y="58" font-size="10.5" letter-spacing=".1em" fill="var(--accent-ink)" font-weight="600">FIRST MILE · booking</text>
<rect x="20" y="70" width="155" height="44" rx="6" fill="var(--accent-soft)" stroke="var(--accent)"/>
<text x="97" y="90" text-anchor="middle" font-size="10.5" fill="var(--accent-ink)">Created |</text>
<text x="97" y="104" text-anchor="middle" font-size="10.5" fill="var(--accent-ink)">Pending_Pickup</text>
<rect x="205" y="70" width="155" height="44" rx="6" fill="var(--accent-soft)" stroke="var(--accent)"/>
<text x="282" y="97" text-anchor="middle" font-size="10.5" fill="var(--accent-ink)">Miler_Assigned</text>
<rect x="390" y="70" width="155" height="44" rx="6" fill="var(--accent-soft)" stroke="var(--accent)"/>
<text x="467" y="97" text-anchor="middle" font-size="10.5" fill="var(--accent-ink)">Pickup_Scheduled</text>
<rect x="575" y="70" width="155" height="44" rx="6" fill="var(--accent-soft)" stroke="var(--accent)"/>
<text x="652" y="97" text-anchor="middle" font-size="10.5" fill="var(--accent-ink)">Arrived_At_Pickup</text>
<rect x="760" y="70" width="155" height="44" rx="6" fill="var(--accent-soft)" stroke="var(--accent)"/>
<text x="837" y="97" text-anchor="middle" font-size="10.5" fill="var(--accent-ink)">Picked_Up</text>
<rect x="945" y="70" width="155" height="44" rx="6" fill="var(--accent-soft)" stroke="var(--accent)" stroke-width="1.8"/>
<text x="1022" y="90" text-anchor="middle" font-size="10.5" fill="var(--accent-ink)">Converted_To_</text>
<text x="1022" y="104" text-anchor="middle" font-size="10.5" fill="var(--accent-ink)">Consignment</text>
<line x1="179" y1="92" x2="201" y2="92" stroke="currentColor" stroke-width="1.4" marker-end="url(#II_w1)"/>
<line x1="364" y1="92" x2="386" y2="92" stroke="currentColor" stroke-width="1.4" marker-end="url(#II_w1)"/>
<line x1="549" y1="92" x2="571" y2="92" stroke="currentColor" stroke-width="1.4" marker-end="url(#II_w1)"/>
<line x1="734" y1="92" x2="756" y2="92" stroke="currentColor" stroke-width="1.4" marker-end="url(#II_w1)"/>
<line x1="919" y1="92" x2="941" y2="92" stroke="currentColor" stroke-width="1.4" marker-end="url(#II_w1)"/>
<!-- drop right -->
<path d="M1022,114 V230" fill="none" stroke="currentColor" stroke-width="1.4" marker-end="url(#II_w1)"/>
<!-- ROW 2 : mid mile -->
<text x="945" y="212" text-anchor="end" font-size="10.5" letter-spacing=".1em" fill="var(--gold)" font-weight="600">MID MILE · consignment</text>
<rect x="945" y="232" width="155" height="44" rx="6" fill="var(--gold-soft)" stroke="var(--gold)"/>
<text x="1022" y="252" text-anchor="middle" font-size="10.5" fill="var(--gold)">Collected_By_</text>
<text x="1022" y="266" text-anchor="middle" font-size="10.5" fill="var(--gold)">Miler</text>
<rect x="760" y="232" width="155" height="44" rx="6" fill="var(--gold-soft)" stroke="var(--gold)"/>
<text x="837" y="259" text-anchor="middle" font-size="10.5" fill="var(--gold)">Inwarded_at_Hub</text>
<rect x="575" y="232" width="155" height="44" rx="6" fill="var(--gold-soft)" stroke="var(--gold)"/>
<text x="652" y="259" text-anchor="middle" font-size="10.5" fill="var(--gold)">Tripsheet_Loaded</text>
<rect x="390" y="232" width="155" height="44" rx="6" fill="var(--gold-soft)" stroke="var(--gold)"/>
<text x="467" y="259" text-anchor="middle" font-size="10.5" fill="var(--gold)">In_Transit</text>
<line x1="941" y1="254" x2="919" y2="254" stroke="currentColor" stroke-width="1.4" marker-end="url(#II_w1)"/>
<line x1="756" y1="254" x2="734" y2="254" stroke="currentColor" stroke-width="1.4" marker-end="url(#II_w1)"/>
<line x1="571" y1="254" x2="549" y2="254" stroke="currentColor" stroke-width="1.4" marker-end="url(#II_w1)"/>
<!-- multi-leg loop -->
<path d="M467,232 V196 H837 V228" fill="none" stroke="var(--gold)" stroke-width="1.3" stroke-dasharray="5 4" marker-end="url(#II_w1g)"/>
<rect x="560" y="182" width="184" height="22" rx="11" fill="var(--surface)" stroke="var(--gold)"/>
<text x="652" y="197" text-anchor="middle" font-size="10" fill="var(--gold)">next hub · multi-leg</text>
<!-- drop left -->
<path d="M467,276 V392" fill="none" stroke="currentColor" stroke-width="1.4" marker-end="url(#II_w1)"/>
<!-- hyperlocal bypass -->
<path d="M1022,114 C 1110,150 1110,400 560,400 H500" fill="none" stroke="var(--alt)" stroke-width="1.6" stroke-dasharray="6 4" marker-end="url(#II_w1t)"/>
<rect x="742" y="386" width="292" height="24" rx="12" fill="var(--alt-soft)" stroke="var(--alt)"/>
<text x="888" y="402" text-anchor="middle" font-size="10" fill="var(--alt)">hyperlocal — pincode prefix match</text>
<!-- ROW 3 : last mile -->
<text x="20" y="378" font-size="10.5" letter-spacing=".1em" fill="var(--alt)" font-weight="600">LAST MILE</text>
<rect x="390" y="394" width="155" height="44" rx="6" fill="var(--alt-soft)" stroke="var(--alt)"/>
<text x="467" y="421" text-anchor="middle" font-size="10.5" fill="var(--alt)">Out_for_Delivery</text>
<line x1="549" y1="416" x2="571" y2="416" stroke="currentColor" stroke-width="1.4" marker-end="url(#II_w1)"/>
<rect x="575" y="394" width="155" height="44" rx="6" fill="var(--alt-soft)" stroke="var(--alt)" stroke-width="2.4"/>
<text x="652" y="421" text-anchor="middle" font-size="11" font-weight="600" fill="var(--alt)">Delivered</text>
<!-- failure branch -->
<path d="M467,438 V486 H756" fill="none" stroke="var(--accent)" stroke-width="1.4" marker-end="url(#II_w1a)"/>
<text x="560" y="478" font-size="10" fill="var(--accent-ink)" font-weight="600">attempts exhausted</text>
<rect x="760" y="464" width="155" height="44" rx="6" fill="var(--accent-soft)" stroke="var(--accent)"/>
<text x="837" y="491" text-anchor="middle" font-size="10.5" fill="var(--accent-ink)">RTO_Initiated</text>
<line x1="919" y1="486" x2="941" y2="486" stroke="var(--accent)" stroke-width="1.4" marker-end="url(#II_w1a)"/>
<rect x="945" y="464" width="155" height="44" rx="6" fill="var(--accent-soft)" stroke="var(--accent)" stroke-width="2.4"/>
<text x="1022" y="484" text-anchor="middle" font-size="10.5" font-weight="600" fill="var(--accent-ink)">Returned_to_</text>
<text x="1022" y="498" text-anchor="middle" font-size="10.5" font-weight="600" fill="var(--accent-ink)">Sender</text>
<!-- skip loop -->
<path d="M467,438 C 360,470 360,400 386,416" fill="none" stroke="var(--accent)" stroke-width="1.3" stroke-dasharray="5 4" marker-end="url(#II_w1a)"/>
<text x="240" y="452" font-size="10" fill="var(--accent-ink)" font-weight="600">skip → Attemptcount + 1</text>
<!-- exception terminals -->
<line x1="20" y1="546" x2="1100" y2="546" stroke="var(--line)" stroke-width="1"/>
<text x="20" y="576" font-size="10.5" letter-spacing=".1em" fill="var(--muted)" font-weight="600">EXCEPTION TERMINALS · reachable from any leg</text>
<rect x="440" y="560" width="120" height="30" rx="6" fill="var(--surface)" stroke="var(--line-2)"/>
<text x="500" y="580" text-anchor="middle" font-size="10.5" fill="var(--ink)">Missing</text>
<rect x="576" y="560" width="120" height="30" rx="6" fill="var(--surface)" stroke="var(--line-2)"/>
<text x="636" y="580" text-anchor="middle" font-size="10.5" fill="var(--ink)">Damaged</text>
<rect x="712" y="560" width="120" height="30" rx="6" fill="var(--surface)" stroke="var(--line-2)"/>
<text x="772" y="580" text-anchor="middle" font-size="10.5" fill="var(--ink)">Cancelled</text>
</g>
</svg>
</div>
<figcaption><b>Figure II.4 — the complete chain</b>Two entities, one chain: the first row is a <code>PickupBooking</code>, everything after <code>Converted_To_Consignment</code> is a <code>Consignment</code>. The teal bypass and the gold loop are the two places a parcel's path stops being linear.</figcaption>
</figure>
<div class="table-scroll">
<table>
<thead><tr><th>Leg</th><th>Entity</th><th>Ends when</th><th>Loops</th><th>Optimised today by</th></tr></thead>
<tbody>
<tr><td>First</td><td><code>PickupBooking</code></td><td>parcel in rider's hands</td><td>reject → reassign</td><td>per-parcel geo-search + LLM pick</td></tr>
<tr><td>Mid</td><td><code>Consignment</code></td><td>at destination hub</td><td>multi-leg re-inward</td><td>nothing — hand-built tripsheets</td></tr>
<tr><td>Last</td><td><code>Consignment</code></td><td>OTP verified, proof filed</td><td>skip → next attempt</td><td>Valhalla sequencing, post-assignment</td></tr>
</tbody>
</table>
</div>
</section>
<section id="ordering">
<div class="sec-head"><span class="sec-num">05</span><div><h2>The ordering mistake</h2></div></div>
<p class="measure">This is the most consequential thing in the codebase, and it is a sequencing question about the software rather than the parcels. Doormile <strong>assigns first and sequences second</strong>. Those are not two problems — they are one problem, and solving them in series throws away most of the available gain.</p>
<figure>
<div class="fig-scroll">
<svg viewBox="0 0 1120 430" role="img" aria-label="Top: today's pipeline runs one parcel at a time through geo-search, an LLM choice, a committed assignment, and only then road sequencing of whatever stops that rider was given. Bottom: the proposed pipeline accumulates a batch of parcels and available riders with their constraints, and a single vehicle-routing solver assigns and sequences together, leaving the LLM to handle exceptions only.">
<defs>
<marker id="II_b" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse"><path d="M0,0 L10,5 L0,10 z" fill="currentColor"/></marker>
<marker id="II_bt" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse"><path d="M0,0 L10,5 L0,10 z" fill="var(--alt)"/></marker>
</defs>
<g font-family="'Source Sans 3', sans-serif" color="var(--line-2)">
<text x="0" y="22" font-family="'JetBrains Mono', monospace" font-size="11" font-weight="600" letter-spacing="1.1" fill="var(--accent-ink)">TODAY · ASSIGN, THEN SEQUENCE</text>
<rect x="0" y="46" width="150" height="58" rx="3" fill="var(--surface-2)" stroke="var(--line-2)"/>
<text x="75" y="72" text-anchor="middle" font-size="12.5" font-weight="600" fill="var(--ink)">one parcel</text>
<text x="75" y="89" text-anchor="middle" font-size="10.5" fill="var(--muted)">as it arrives</text>
<line x1="154" y1="75" x2="192" y2="75" stroke="currentColor" stroke-width="1.4" marker-end="url(#II_b)"/>
<rect x="196" y="46" width="184" height="58" rx="3" fill="var(--surface-2)" stroke="var(--line-2)"/>
<text x="288" y="70" text-anchor="middle" font-size="12.5" font-weight="600" fill="var(--ink)">GEOSEARCH</text>
<text x="288" y="88" text-anchor="middle" font-size="10.5" font-family="'JetBrains Mono', monospace" fill="var(--muted)">10 km · top 10</text>
<line x1="384" y1="75" x2="422" y2="75" stroke="currentColor" stroke-width="1.4" marker-end="url(#II_b)"/>
<rect x="426" y="46" width="184" height="58" rx="3" fill="var(--accent-soft)" stroke="var(--accent)"/>
<text x="518" y="70" text-anchor="middle" font-size="12.5" font-weight="600" fill="var(--accent-ink)">LLM picks one</text>
<text x="518" y="88" text-anchor="middle" font-size="10.5" fill="var(--accent-ink)">5 s cap</text>
<line x1="614" y1="75" x2="652" y2="75" stroke="currentColor" stroke-width="1.4" marker-end="url(#II_b)"/>
<rect x="656" y="46" width="160" height="58" rx="3" fill="var(--surface-2)" stroke="var(--ink)" stroke-width="1.5"/>
<text x="736" y="70" text-anchor="middle" font-size="12.5" font-weight="600" fill="var(--ink)">assignment</text>
<text x="736" y="88" text-anchor="middle" font-size="10.5" fill="var(--muted)">committed</text>
<line x1="820" y1="75" x2="858" y2="75" stroke="currentColor" stroke-width="1.4" marker-end="url(#II_b)"/>
<rect x="862" y="46" width="214" height="58" rx="3" fill="var(--surface)" stroke="var(--line-2)" stroke-dasharray="4 3"/>
<text x="969" y="70" text-anchor="middle" font-size="12.5" font-weight="600" fill="var(--ink)">Valhalla sequences</text>
<text x="969" y="88" text-anchor="middle" font-size="10.5" fill="var(--muted)">that rider's stops, async</text>
<path d="M969,108 V138 H518 V108" fill="none" stroke="var(--accent)" stroke-width="1.3" stroke-dasharray="5 4"/>
<rect x="600" y="126" width="330" height="24" rx="3" fill="var(--accent-soft)" stroke="var(--accent)"/>
<text x="765" y="143" text-anchor="middle" font-size="11" font-weight="600" fill="var(--accent-ink)">can only reorder a set it did not get to choose</text>
<line x1="0" y1="196" x2="1120" y2="196" stroke="var(--line)" stroke-width="1"/>
<text x="0" y="232" font-family="'JetBrains Mono', monospace" font-size="11" font-weight="600" letter-spacing="1.1" fill="var(--alt)">PROPOSED · ONE JOINT SOLVE</text>
<rect x="0" y="256" width="200" height="42" rx="3" fill="var(--surface-2)" stroke="var(--line-2)"/>
<text x="100" y="283" text-anchor="middle" font-size="12" fill="var(--ink)">N parcels · batch window</text>
<rect x="0" y="306" width="200" height="42" rx="3" fill="var(--surface-2)" stroke="var(--line-2)"/>
<text x="100" y="333" text-anchor="middle" font-size="12" fill="var(--ink)">M riders · live capacity</text>
<rect x="0" y="356" width="200" height="42" rx="3" fill="var(--surface-2)" stroke="var(--line-2)"/>
<text x="100" y="376" text-anchor="middle" font-size="12" fill="var(--ink)">constraints</text>
<text x="100" y="391" text-anchor="middle" font-size="10" fill="var(--muted)">slots · capacity · skills</text>
<path d="M204,277 H244 V327" fill="none" stroke="var(--alt)" stroke-width="1.4"/>
<path d="M204,327 H244" fill="none" stroke="var(--alt)" stroke-width="1.4"/>
<path d="M204,377 H244 V327" fill="none" stroke="var(--alt)" stroke-width="1.4"/>
<line x1="244" y1="327" x2="288" y2="327" stroke="var(--alt)" stroke-width="1.5" marker-end="url(#II_bt)"/>
<rect x="292" y="272" width="270" height="110" rx="3" fill="var(--alt-soft)" stroke="var(--alt)" stroke-width="1.6"/>
<text x="427" y="308" text-anchor="middle" font-size="14" font-weight="600" fill="var(--alt)">VRP solver</text>
<text x="427" y="329" text-anchor="middle" font-size="11.5" fill="var(--alt)">assignment and order</text>
<text x="427" y="346" text-anchor="middle" font-size="11.5" fill="var(--alt)">decided together</text>
<text x="427" y="367" text-anchor="middle" font-size="10.5" font-family="'JetBrains Mono', monospace" fill="var(--alt)">OR-Tools / VROOM · ms</text>
<line x1="566" y1="327" x2="606" y2="327" stroke="var(--alt)" stroke-width="1.5" marker-end="url(#II_bt)"/>
<rect x="610" y="272" width="200" height="110" rx="3" fill="var(--surface-2)" stroke="var(--ink)" stroke-width="1.5"/>
<text x="710" y="316" text-anchor="middle" font-size="13" font-weight="600" fill="var(--ink)">complete routes</text>
<text x="710" y="336" text-anchor="middle" font-size="10.5" font-family="'JetBrains Mono', monospace" fill="var(--muted)">rider · step · ETA</text>
<line x1="814" y1="327" x2="854" y2="327" stroke="currentColor" stroke-width="1.4" marker-end="url(#II_b)"/>
<rect x="858" y="272" width="218" height="110" rx="3" fill="var(--accent-soft)" stroke="var(--accent)"/>
<text x="967" y="306" text-anchor="middle" font-size="13" font-weight="600" fill="var(--accent-ink)">LLM</text>
<text x="967" y="327" text-anchor="middle" font-size="11.5" fill="var(--accent-ink)">exceptions only</text>
<text x="967" y="345" text-anchor="middle" font-size="10.5" fill="var(--accent-ink)">overflow · absence ·</text>
<text x="967" y="361" text-anchor="middle" font-size="10.5" fill="var(--accent-ink)">bad address · breakdown</text>
</g>
</svg>
</div>
<figcaption><b>Figure II.5 — the fix is an ordering change</b>The Valhalla client already exists and already speaks Doormile's vocabulary. Moving it in front of assignment, and feeding it the whole open set rather than one rider's inherited stops, is a smaller change than it sounds.</figcaption>
</figure>
<div class="callout">
<span class="label">Why this matters more than model quality</span>
<p>Greedy nearest-neighbour assignment is a known-bad heuristic for vehicle routing — on realistic instances it lands well short of what a proper solver finds on the same data, and no amount of smarter per-parcel scoring recovers the gap, because the loss comes from committing to each parcel before seeing the rest. A better model choosing one rider at a time is still choosing one rider at a time.</p>
</div>
</section>
<section id="clocks">
<div class="sec-head"><span class="sec-num">06</span><div><h2>Put the intelligence where it belongs</h2></div></div>
<p class="measure">The expert move is to stop treating "AI" as one layer. Logistics decisions live on three clocks, and each clock wants a different technique. Doormile currently has a language model on the second clock, which is the one place it fits worst.</p>
<figure>
<div class="fig-scroll">
<svg viewBox="0 0 1120 480" role="img" aria-label="Three stacked horizons. Strategic, monthly: beat and territory design, hub and lane network, solved by districting and facility location. Tactical, per wave: load planning, cutoffs, joint assignment and sequencing, solved by a mixed-integer or vehicle-routing solver. Operational, per second: exceptions such as overflow, absence, bad addresses, handled by a language model. A feedback arrow runs from completed-route data back up into all three layers.">
<defs>
<marker id="II_c" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse"><path d="M0,0 L10,5 L0,10 z" fill="currentColor"/></marker>
</defs>
<g font-family="'Source Sans 3', sans-serif" color="var(--line-2)">
<text x="0" y="24" font-family="'JetBrains Mono', monospace" font-size="10.5" letter-spacing=".08em" fill="var(--muted)">HORIZON</text>
<text x="230" y="24" font-family="'JetBrains Mono', monospace" font-size="10.5" letter-spacing=".08em" fill="var(--muted)">DECIDES</text>
<text x="600" y="24" font-family="'JetBrains Mono', monospace" font-size="10.5" letter-spacing=".08em" fill="var(--muted)">TECHNIQUE</text>
<text x="960" y="24" font-family="'JetBrains Mono', monospace" font-size="10.5" letter-spacing=".08em" fill="var(--muted)">CADENCE</text>
<rect x="0" y="40" width="1000" height="94" rx="3" fill="var(--surface-2)" stroke="var(--line-2)"/>
<rect x="0" y="40" width="5" height="94" fill="var(--alt)"/>
<text x="24" y="76" font-size="14" font-weight="600" fill="var(--ink)">Strategic</text>
<text x="24" y="99" font-size="11" fill="var(--muted)">the shape of the network</text>
<text x="230" y="72" font-size="12.5" fill="var(--ink)">beat &amp; territory design · hub siting</text>
<text x="230" y="92" font-size="12.5" fill="var(--ink)">which lanes exist at all</text>
<text x="230" y="112" font-size="12.5" fill="var(--ink)">rider headcount per zone</text>
<text x="600" y="82" font-size="12.5" fill="var(--alt)">districting · facility location</text>
<text x="600" y="102" font-size="12.5" fill="var(--alt)">demand forecasting</text>
<rect x="940" y="74" width="130" height="26" rx="13" fill="var(--surface)" stroke="var(--line-2)"/>
<text x="1005" y="92" text-anchor="middle" font-size="11" font-family="'JetBrains Mono', monospace" fill="var(--ink)">monthly</text>
<rect x="0" y="146" width="1000" height="114" rx="3" fill="var(--alt-soft)" stroke="var(--alt)" stroke-width="1.6"/>
<rect x="0" y="146" width="5" height="114" fill="var(--alt)"/>
<text x="24" y="184" font-size="14" font-weight="600" fill="var(--ink)">Tactical</text>
<text x="24" y="207" font-size="11" fill="var(--muted)">the plan for this wave</text>
<text x="24" y="230" font-size="10.5" font-family="'JetBrains Mono', monospace" fill="var(--alt)">← the LLM sits</text>
<text x="24" y="245" font-size="10.5" font-family="'JetBrains Mono', monospace" fill="var(--alt)"> here today</text>
<text x="230" y="180" font-size="12.5" fill="var(--ink)">joint assignment + sequencing</text>
<text x="230" y="200" font-size="12.5" fill="var(--ink)">tripsheet load planning</text>
<text x="230" y="220" font-size="12.5" fill="var(--ink)">hold-vs-go on every departure</text>
<text x="230" y="240" font-size="12.5" fill="var(--ink)">rider-to-beat for the day</text>
<text x="600" y="190" font-size="12.5" font-weight="600" fill="var(--alt)">MILP / VRP solver</text>
<text x="600" y="210" font-size="12.5" fill="var(--alt)">OR-Tools, VROOM, Valhalla matrix</text>
<text x="600" y="230" font-size="11.5" fill="var(--muted)">deterministic · explainable · milliseconds</text>
<rect x="940" y="190" width="130" height="26" rx="13" fill="var(--surface)" stroke="var(--alt)"/>
<text x="1005" y="208" text-anchor="middle" font-size="11" font-family="'JetBrains Mono', monospace" fill="var(--alt)">per wave</text>
<rect x="0" y="272" width="1000" height="94" rx="3" fill="var(--accent-soft)" stroke="var(--accent)"/>
<rect x="0" y="272" width="5" height="94" fill="var(--accent)"/>
<text x="24" y="308" font-size="14" font-weight="600" fill="var(--ink)">Operational</text>
<text x="24" y="331" font-size="11" fill="var(--muted)">what just went wrong</text>
<text x="230" y="304" font-size="12.5" fill="var(--ink)">unresolvable address · receiver unreachable</text>
<text x="230" y="324" font-size="12.5" fill="var(--ink)">rider absent · vehicle down · overflow parcel</text>
<text x="230" y="344" font-size="12.5" fill="var(--ink)">operator asks a question in plain English</text>
<text x="600" y="314" font-size="12.5" font-weight="600" fill="var(--accent-ink)">LLM + rules</text>
<text x="600" y="334" font-size="11.5" fill="var(--accent-ink)">judgment over messy, unstructured input</text>
<rect x="940" y="306" width="130" height="26" rx="13" fill="var(--surface)" stroke="var(--accent)"/>
<text x="1005" y="324" text-anchor="middle" font-size="11" font-family="'JetBrains Mono', monospace" fill="var(--accent-ink)">per second</text>
<rect x="0" y="404" width="1000" height="58" rx="3" fill="var(--surface)" stroke="var(--ink)" stroke-width="1.5"/>
<text x="24" y="428" font-size="13" font-weight="600" fill="var(--ink)">Completed-route exhaust</text>
<text x="24" y="448" font-size="11.5" fill="var(--muted)">AgentDecision · breadcrumbs · arrivedat + GPS · Attemptcount · DeliveryProof geolocation</text>
<text x="700" y="440" font-size="12" font-weight="600" fill="var(--ink)">→ learned dwell, travel time, failure risk</text>
<path d="M1040,433 H1090 V60 H1006" fill="none" stroke="currentColor" stroke-width="1.5" marker-end="url(#II_c)"/>
<path d="M1090,203 H1006" fill="none" stroke="currentColor" stroke-width="1.5" marker-end="url(#II_c)"/>
<path d="M1090,319 H1006" fill="none" stroke="currentColor" stroke-width="1.5" marker-end="url(#II_c)"/>
<text x="1104" y="250" font-size="11" font-weight="600" fill="var(--ink)" transform="rotate(90 1104 250)">trains every layer</text>
</g>
</svg>
</div>
<figcaption><b>Figure II.6 — three clocks, three techniques</b>Language models are strong on unstructured judgment and weak on combinatorial search. Solvers are the reverse. The current design has them swapped.</figcaption>
</figure>
<h3>Per leg, what to build</h3>
<ul class="plain">
<li><b>First — batch the window, solve once.</b> Accumulate 10–15 minutes of open pickups and solve assignment and sequence together. Make the candidate set adaptive: widen the radius until <em>k</em> genuine candidates are found, instead of a fixed 10 km that returns ten near-identical riders at noon and nothing at 6 AM. Learn dwell time per pickup point — every <code>arrivedat</code> → <code>pickup-complete</code> pair is already a labelled example.</li>
<li><b>Mid — auto-build the tripsheet.</b> Parcel-to-truck given capacity, destination mix and cut-off is bin-packing with a clean objective. Then price hold-vs-go against <code>sladueat</code>, and consolidate thin lanes through a transfer hub rather than running half-empty direct trips — <code>Batchkind</code>'s <code>local</code>/<code>transfer</code> split already anticipates this.</li>
<li><b>Last — predict first-attempt success and sequence on it.</b> <code>Attemptcount</code> and skip reasons are your training labels. Then resolve addresses with the pgvector you already run: embedding delivery addresses and snapping them to confirmed <code>DeliveryProof</code> coordinates turns your delivered history into a private geocoder that beats any general one inside your own zones.</li>
</ul>
<div class="callout teal">
<span class="label">Replace this function first</span>
<p><code>calculateETA</code> is <code>(distance / 20) × 60 + 10</code> — a flat 20 km/h against straight-line distance, plus a fixed ten-minute buffer. It sets rider expectations, customer-facing ETAs and support load. Road time from the Valhalla matrix you already pay for, multiplied by a time-of-day factor and added to learned dwell, is a same-day change with effect on every leg.</p>
</div>
</section>
<section id="build">
<div class="sec-head"><span class="sec-num">07</span><div><h2>Build order</h2></div></div>
<p class="measure">Sequenced by ratio of effect to effort, not by ambition. The first two need no new infrastructure at all.</p>
<div class="table-scroll">
<table>
<thead><tr><th>#</th><th>Move</th><th>Leg</th><th>Depends on</th><th>Why it's here</th></tr></thead>
<tbody>
<tr><td class="num">1</td><td>Score <code>AgentDecision</code> against outcomes</td><td>all</td><td>nothing — data exists</td><td>You cannot currently tell whether AI dispatch beats greedy-nearest. Until you can, every other change is unmeasurable.</td></tr>
<tr><td class="num">2</td><td>Real ETA from road time + learned dwell</td><td>all</td><td>Valhalla matrix</td><td>Replaces a flat 20 km/h constant. Touches rider trust, customer ETA and support volume at once.</td></tr>
<tr><td class="num">3</td><td>Adaptive candidate set</td><td>first</td><td>nothing</td><td>Removes both failure modes of the fixed 10 km / top 10 window.</td></tr>
<tr><td class="num">4</td><td>Batched joint assign + sequence</td><td>first, last</td><td>solver, batch window</td><td>The ordering fix in §05. Largest routing gain available.</td></tr>
<tr><td class="num">5</td><td>Address resolution on pgvector</td><td>last</td><td>delivered history</td><td>Turns your own proof-of-delivery coordinates into a private geocoder for your zones.</td></tr>
<tr><td class="num">6</td><td>First-attempt success model</td><td>last</td><td>#5, <code>Attemptcount</code> history</td><td>Attacks the dominant cost line in last mile.</td></tr>
<tr><td class="num">7</td><td>Automated tripsheet load planning</td><td>mid</td><td>#2, volume forecast</td><td>The only leg with no optimisation at all today.</td></tr>
<tr><td class="num">8</td><td>Hold-vs-go and dynamic cut-offs</td><td>mid</td><td>#7, <code>sladueat</code></td><td>Converts a dispatcher's guess into a priced decision.</td></tr>
<tr><td class="num">9</td><td>Beat districting</td><td>first, last</td><td>#4, a Beat entity</td><td>The strategic layer. Only worth it once daily routing is solid.</td></tr>
</tbody>
</table>
</div>
<div class="callout">
<span class="label">The milk-round dividend</span>
<p>Every item above gets cheaper under a round model rather than more expensive. When stops are stable, the heavy optimisation moves to beat-design time — monthly, offline, with as much compute as you like — and the daily solve shrinks to the deltas: today's new stops, today's skips, today's absences. Real-time combinatorial search over the whole city, every wave, is a cost you only pay because the beats do not exist yet.</p>
</div>
</section>
<div class="part">
<p class="part-kicker">Part III</p>
<h2 class="part-title">Doormile Agent Layer</h2>
<p class="part-intro">Where generative AI genuinely earns its place in the platform — auto-assignment included — how it plugs into the Go backend you already have, and the trust ladder that gets it from <strong>logging quietly</strong> to <strong>acting alone</strong> without a bad week in between.</p>
</div>
<section id="rule">
<div class="sec-head"><span class="sec-num">00</span><div><h2>The rule that makes all of this work</h2></div></div>
<p class="measure" style="font-size:1.22rem; line-height:1.5; color:var(--ink);"><strong>The model proposes. The solver decides. The system validates.</strong></p>
<p class="measure">Every capability below is an application of that one sentence. A language model is exceptional at turning mess into structure — a dispatcher's sentence into a constraint, a WhatsApp forward into a booking, a photo into a condition code, a solver's output into an explanation a rider will actually read. It is poor and expensive at choosing the best of ten thousand arrangements, which is exactly what dispatch is.</p>
<p class="measure">So "auto-assign with GenAI" does not mean a model picking riders. It means a model doing the four things around the pick that were previously impossible to automate, while a solver does the pick itself in milliseconds and a validator refuses anything that breaks an invariant. That is a more capable system than an LLM choosing alone, and a cheaper one.</p>
<div class="callout teal">
<span class="label">You already got the hard part right</span>
<p>The payload your backend sends to <code>routemate</code> is entirely structured — <code>miler_id</code>, <code>distance_km</code>, <code>rating</code>, <code>on_time_rate_30d</code>, <code>hub_load</code>, <code>hub_capacity</code>, plus hour, peak flag and zone. No customer-supplied free text goes into it. That closed payload is a security property, and §06 explains why keeping it is the single most important rule as you expand.</p>
</div>
</section>
<section id="map">
<div class="sec-head"><span class="sec-num">01</span><div><h2>Where GenAI actually earns its place</h2></div></div>
<p class="measure">Ten capabilities, ranked by what they return for what they cost. Note how few of them are "the AI decides" and how many are "the AI turns something unstructured into something the system can already handle."</p>
<div class="table-scroll">
<table>
<thead><tr><th>#</th><th>Capability</th><th>Leg</th><th>What the model does</th><th>Value</th><th>Effort</th></tr></thead>
<tbody>
<tr><td class="num">1</td><td><b>Address resolution</b></td><td>last</td><td>Embeds messy Indian address text, clusters duplicates, snaps to confirmed <code>DeliveryProof</code> coordinates. Builds you a private geocoder from your own delivered history.</td><td><span class="pill p-high">highest</span></td><td><span class="pill p-low">low</span></td></tr>
<tr><td class="num">2</td><td><b>Decision explanation</b></td><td>all</td><td>Turns solver output into a sentence for the dispatcher and a line for the rider. Drives adoption more than accuracy does.</td><td><span class="pill p-high">high</span></td><td><span class="pill p-low">low</span></td></tr>
<tr><td class="num">3</td><td><b>Unstructured intake</b></td><td>first</td><td>A client's WhatsApp message, email or PDF manifest becomes structured bookings. You support Excel; clients don't always send Excel.</td><td><span class="pill p-high">high</span></td><td><span class="pill p-mid">medium</span></td></tr>
<tr><td class="num">4</td><td><b>NL constraint compiler</b></td><td>all</td><td>"Don't give Ravi the mall runs" becomes a typed solver constraint a human approves once and the solver honours forever.</td><td><span class="pill p-high">high</span></td><td><span class="pill p-mid">medium</span></td></tr>
<tr><td class="num">5</td><td><b>Exception agent</b></td><td>all</td><td>Rider absent, vehicle down, forty parcels stranded. Tool-calling over a bounded action space, proposing a recovery plan.</td><td><span class="pill p-high">high</span></td><td><span class="pill p-mid">medium</span></td></tr>
<tr><td class="num">6</td><td><b>Damage detection at inbound</b></td><td>mid</td><td>Vision on the inbound scan photo auto-populates <code>Condition</code>, which is a manual dropdown today and therefore mostly says <code>Good</code>.</td><td><span class="pill p-mid">medium</span></td><td><span class="pill p-low">low</span></td></tr>
<tr><td class="num">7</td><td><b>POD verification</b></td><td>last</td><td>Vision checks the delivery photo actually shows a parcel at a door, and that the signature isn't blank. Catches soft fraud.</td><td><span class="pill p-mid">medium</span></td><td><span class="pill p-low">low</span></td></tr>
<tr><td class="num">8</td><td><b>Vernacular voice</b></td><td>first, last</td><td>Rider status by speech in Tamil, Telugu, Kannada or Hindi; an outbound call confirming the consignee will be home. Directly attacks first-attempt failure.</td><td><span class="pill p-high">high</span></td><td><span class="pill p-mid">high</span></td></tr>
<tr><td class="num">9</td><td><b>Operator copilot</b></td><td>all</td><td>Extends the existing console assistant from read-only queries to proposing actions with confirmation.</td><td><span class="pill p-mid">medium</span></td><td><span class="pill p-mid">medium</span></td></tr>
<tr><td class="num">10</td><td><b>Scenario simulation</b></td><td>all</td><td>Replays historical days against a proposed policy before it touches production. Your safety net for everything above.</td><td><span class="pill p-mid">medium</span></td><td><span class="pill p-mid">medium</span></td></tr>
</tbody>
</table>
</div>
<p class="measure">Four hubs across Coimbatore, Hyderabad, Bengaluru and Chennai means four language regions. Item 8 is not a nicety in that footprint — a rider who can speak a status update is a rider who actually files one.</p>
</section>
<section id="assign">
<div class="sec-head"><span class="sec-num">02</span><div><h2>Auto-assign, properly</h2></div></div>
<p class="measure">This is the thing you asked about, drawn end to end. The deterministic spine runs down the left and never waits on a model. The three GenAI touch points sit beside it, feeding in out of band.</p>
<figure>
<div class="fig-scroll">
<svg viewBox="0 0 1120 780" role="img" aria-label="Auto-assign architecture. A deterministic spine runs down the left: booking enqueued on NATS, adaptive candidate generation from Redis, constraint set assembled, a vehicle routing solver using the Valhalla matrix, a validator enforcing closed-set selection and tenant isolation, then a committed transaction. Three generative AI services sit to the right and feed in out of band: a natural-language constraint compiler that a human approves once, an exception agent invoked only when the solve is infeasible, and an explainer that writes reasoning after the commit. None of them sit on the critical path.">
<defs>
<marker id="III_g1" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse"><path d="M0,0 L10,5 L0,10 z" fill="currentColor"/></marker>
<marker id="III_g1a" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse"><path d="M0,0 L10,5 L0,10 z" fill="var(--accent)"/></marker>
<marker id="III_g1t" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse"><path d="M0,0 L10,5 L0,10 z" fill="var(--alt)"/></marker>
</defs>
<g font-family="'Source Sans 3', sans-serif" color="var(--line-2)">
<rect x="0" y="34" width="580" height="716" rx="4" fill="var(--surface-2)" opacity=".55"/>
<text x="20" y="22" font-family="'JetBrains Mono', monospace" font-size="10.5" letter-spacing=".1em" font-weight="600" fill="var(--alt)">DETERMINISTIC SPINE · never waits on a model</text>
<text x="660" y="22" font-family="'JetBrains Mono', monospace" font-size="10.5" letter-spacing=".1em" font-weight="600" fill="var(--accent-ink)">GENERATIVE · out of band</text>
<!-- spine boxes -->
<rect x="40" y="52" width="380" height="52" rx="3" fill="var(--surface)" stroke="var(--line-2)"/>
<text x="230" y="83" text-anchor="middle" font-size="12.5" fill="var(--ink)">booking enqueued · NATS</text>
<line x1="230" y1="104" x2="230" y2="128" stroke="currentColor" stroke-width="1.4" marker-end="url(#III_g1)"/>
<rect x="40" y="130" width="380" height="70" rx="3" fill="var(--surface)" stroke="var(--line-2)"/>
<text x="230" y="154" text-anchor="middle" font-size="12.5" font-weight="600" fill="var(--ink)">candidate generation</text>
<text x="230" y="173" text-anchor="middle" font-size="10.5" font-family="'JetBrains Mono', monospace" fill="var(--muted)">Redis GEOSEARCH · adaptive k</text>
<text x="230" y="189" text-anchor="middle" font-size="10.5" fill="var(--muted)">widen radius until k found</text>
<line x1="230" y1="200" x2="230" y2="224" stroke="currentColor" stroke-width="1.4" marker-end="url(#III_g1)"/>
<rect x="40" y="226" width="380" height="70" rx="3" fill="var(--surface)" stroke="var(--line-2)"/>
<text x="230" y="250" text-anchor="middle" font-size="12.5" font-weight="600" fill="var(--ink)">constraint set assembled</text>
<text x="230" y="269" text-anchor="middle" font-size="10.5" fill="var(--muted)">capacity · slots · skills · tenant scope</text>
<text x="230" y="285" text-anchor="middle" font-size="10.5" fill="var(--muted)">+ approved rules from the compiler</text>
<line x1="230" y1="296" x2="230" y2="320" stroke="currentColor" stroke-width="1.4" marker-end="url(#III_g1)"/>
<rect x="40" y="322" width="380" height="82" rx="3" fill="var(--alt-soft)" stroke="var(--alt)" stroke-width="1.8"/>
<text x="230" y="348" text-anchor="middle" font-size="14" font-weight="600" fill="var(--alt)">VRP solver</text>
<text x="230" y="368" text-anchor="middle" font-size="11" fill="var(--alt)">assign + sequence, jointly</text>
<text x="230" y="386" text-anchor="middle" font-size="10.5" font-family="'JetBrains Mono', monospace" fill="var(--alt)">Valhalla matrix · milliseconds</text>
<line x1="230" y1="404" x2="230" y2="428" stroke="currentColor" stroke-width="1.4" marker-end="url(#III_g1)"/>
<rect x="40" y="430" width="380" height="88" rx="3" fill="var(--surface)" stroke="var(--ink)" stroke-width="2"/>
<text x="230" y="454" text-anchor="middle" font-size="13" font-weight="600" fill="var(--ink)">VALIDATOR — hard gate</text>
<text x="230" y="474" text-anchor="middle" font-size="10.5" fill="var(--muted)">miler_id ∈ candidate set?</text>
<text x="230" y="490" text-anchor="middle" font-size="10.5" fill="var(--muted)">tenant scope intact? capacity respected?</text>
<text x="230" y="506" text-anchor="middle" font-size="10.5" fill="var(--muted)">reject → greedy nearest, always</text>
<line x1="230" y1="518" x2="230" y2="542" stroke="currentColor" stroke-width="1.4" marker-end="url(#III_g1)"/>
<rect x="40" y="544" width="380" height="70" rx="3" fill="var(--surface)" stroke="var(--ink)" stroke-width="1.5"/>
<text x="230" y="568" text-anchor="middle" font-size="12.5" font-weight="600" fill="var(--ink)">commitAssignment</text>
<text x="230" y="587" text-anchor="middle" font-size="10.5" font-family="'JetBrains Mono', monospace" fill="var(--muted)">one transaction · Idempotency-Key</text>
<text x="230" y="603" text-anchor="middle" font-size="10.5" fill="var(--muted)">booking + assignment + miler status</text>
<line x1="230" y1="614" x2="230" y2="638" stroke="currentColor" stroke-width="1.4" marker-end="url(#III_g1)"/>
<rect x="40" y="640" width="380" height="70" rx="3" fill="var(--surface)" stroke="var(--line-2)"/>
<text x="230" y="664" text-anchor="middle" font-size="12.5" font-weight="600" fill="var(--ink)">AgentDecision written</text>
<text x="230" y="683" text-anchor="middle" font-size="10.5" font-family="'JetBrains Mono', monospace" fill="var(--muted)">Context · Decision · Reasoning</text>
<text x="230" y="699" text-anchor="middle" font-size="10.5" font-family="'JetBrains Mono', monospace" fill="var(--muted)">Outcome ← filled later, see §05</text>
<!-- GenAI 1: constraint compiler -->
<rect x="640" y="130" width="440" height="98" rx="3" fill="var(--accent-soft)" stroke="var(--accent)"/>
<text x="660" y="156" font-size="12.5" font-weight="600" fill="var(--accent-ink)">① NL constraint compiler</text>
<text x="660" y="176" font-size="11" fill="var(--accent-ink)">dispatcher types: "Ravi shouldn't take mall runs"</text>
<text x="660" y="194" font-size="11" fill="var(--accent-ink)">→ typed constraint → <tspan font-weight="600">human approves once</tspan></text>
<text x="660" y="214" font-size="10.5" font-family="'JetBrains Mono', monospace" fill="var(--muted)">offline · stored · reused every solve</text>
<path d="M636,261 H470 V261 H424" fill="none" stroke="var(--accent)" stroke-width="1.4" stroke-dasharray="5 4" marker-end="url(#III_g1a)"/>
<path d="M860,228 V261 H636" fill="none" stroke="var(--accent)" stroke-width="1.4" stroke-dasharray="5 4"/>
<!-- GenAI 2: exception agent -->
<rect x="640" y="322" width="440" height="112" rx="3" fill="var(--accent-soft)" stroke="var(--accent)"/>
<text x="660" y="348" font-size="12.5" font-weight="600" fill="var(--accent-ink)">② Exception agent</text>
<text x="660" y="368" font-size="11" fill="var(--accent-ink)">invoked only when the solve is infeasible</text>
<text x="660" y="386" font-size="11" fill="var(--accent-ink)">or an operator escalates</text>
<text x="660" y="406" font-size="10.5" font-family="'JetBrains Mono', monospace" fill="var(--muted)">tools: reassign · split · defer · widen · page human</text>
<text x="660" y="424" font-size="10.5" font-family="'JetBrains Mono', monospace" fill="var(--muted)">proposals go back through the validator</text>
<path d="M424,378 H636" fill="none" stroke="var(--accent)" stroke-width="1.4" marker-end="url(#III_g1a)"/>
<text x="530" y="370" text-anchor="middle" font-size="10" font-weight="600" fill="var(--accent-ink)">infeasible</text>
<path d="M860,434 V474 H424" fill="none" stroke="var(--accent)" stroke-width="1.4" marker-end="url(#III_g1a)"/>
<text x="600" y="466" text-anchor="middle" font-size="10" font-weight="600" fill="var(--accent-ink)">proposal — never a direct write</text>
<!-- GenAI 3: explainer -->
<rect x="640" y="596" width="440" height="98" rx="3" fill="var(--accent-soft)" stroke="var(--accent)"/>
<text x="660" y="622" font-size="12.5" font-weight="600" fill="var(--accent-ink)">③ Explainer</text>
<text x="660" y="642" font-size="11" fill="var(--accent-ink)">solver output → a sentence for the dispatcher,</text>
<text x="660" y="660" font-size="11" fill="var(--accent-ink)">a line for the rider, in their language</text>
<text x="660" y="680" font-size="10.5" font-family="'JetBrains Mono', monospace" fill="var(--muted)">async · after commit · failure is cosmetic</text>
<path d="M424,600 H636" fill="none" stroke="var(--accent)" stroke-width="1.4" stroke-dasharray="5 4" marker-end="url(#III_g1a)"/>
<!-- timing note -->
<rect x="40" y="726" width="1040" height="30" rx="3" fill="var(--alt-soft)" stroke="var(--alt)"/>
<text x="560" y="746" text-anchor="middle" font-size="11.5" font-weight="600" fill="var(--alt)">critical path budget: candidate generation + solve + validate + commit, with no network call to a model in it</text>
</g>
</svg>
</div>
<figcaption><b>Figure III.1 — auto-assign with generative AI in its right places</b>Compare with today: a model on the critical path with a 5-second cap, choosing one rider for one parcel. Here the model never blocks a booking, and the thing it produces — a constraint, a recovery plan, an explanation — is something no solver could have produced instead.</figcaption>
</figure>
<h3>Why the constraint compiler is the sleeper</h3>
<p class="measure">Dispatchers hold dozens of rules in their heads that never reach the system: which rider is slow at a particular gate, which client insists on the same face, which lane floods in monsoon. Today the only way to encode those is a developer ticket, so they stay in the dispatcher's head and the optimiser looks stupid. A model that turns a typed sentence into a reviewable constraint — which a human approves before it ever binds — converts tacit knowledge into solver input at conversational speed. That is a capability the solver cannot have on its own, and it is where generative AI is doing something genuinely irreplaceable.</p>
<pre>// what the compiler emits — reviewed by a human, then stored
{
"type": "exclude_miler_from_location_class",
"miler_id": 412,
"location_class": "mall_service_gate",
"reason": "slow at gate handover",
"author": "dispatcher:coimbatore:7",
"expires_at": null
}</pre>
</section>
<section id="wiring">
<div class="sec-head"><span class="sec-num">03</span><div><h2>Wiring it into what you have</h2></div></div>
<p class="measure">You do not need new infrastructure. Every piece below already exists in the estate; the work is contracts and placement.</p>
<div class="table-scroll">
<table>
<thead><tr><th>Piece</th><th>Already there</th><th>What changes</th></tr></thead>
<tbody>
<tr><td>Async invocation</td><td>NATS JetStream, the booking worker, a 60 s budget</td><td>Model calls become subscribers, not inline HTTP. Nothing in a request path waits on one.</td></tr>
<tr><td>Fallback discipline</td><td><code>AI_LAYER_FALLBACK</code> and the 5 s cap in <code>selectMilerWithAI</code></td><td>Keep the pattern exactly; apply it to every new capability. Best-effort or it does not ship.</td></tr>
<tr><td>Audit</td><td><code>AgentDecision</code> with a <code>DecisionType</code> discriminator</td><td>One row per model call, whatever the capability. The discriminator was clearly built for this.</td></tr>
<tr><td>Idempotency</td><td>Redis-backed <code>Idempotency-Key</code> on payment, pickup and deliver</td><td>Extend to every agent-initiated write. An agent that retries must not double-act.</td></tr>
<tr><td>Vector store</td><td>pgvector, in the database already</td><td>Address embeddings land here. No new dependency.</td></tr>
<tr><td>Road distance</td><td>Valhalla behind <code>routes.workolik.com</code></td><td>Becomes the solver's cost matrix rather than a post-hoc sequencer.</td></tr>
<tr><td>Object storage</td><td>presigned uploads for signatures and photos</td><td>The same URLs feed the vision capabilities. Images never transit your API.</td></tr>
<tr><td>Console surface</td><td>the existing deterministic assistant and intents</td><td>Gains an action tier behind a confirmation step.</td></tr>
</tbody>
</table>
</div>
<p class="measure">One deliberate constraint: <b>the agent layer never writes to the database directly.</b> It calls the same internal endpoints a human operator would, so tenant scoping, idempotency, validation and audit apply identically whether the caller is a person or a model. If an agent can reach a table your dispatcher cannot, you have built two systems and will secure only one.</p>
</section>
<section id="ladder">
<div class="sec-head"><span class="sec-num">04</span><div><h2>The trust ladder</h2></div></div>
<p class="measure">Every capability climbs the same four rungs, and each rung has a numeric gate. Nothing goes to the next rung on a good feeling.</p>
<figure>
<div class="fig-scroll">
<svg viewBox="0 0 1120 480" role="img" aria-label="A four-rung trust ladder ascending left to right. Rung one, shadow: the model runs and logs but changes nothing, gated on a sufficient volume of logged decisions and a measured agreement rate. Rung two, advisory: the recommendation appears in the console and a dispatcher clicks to apply it, gated on acceptance rate and zero invariant violations. Rung three, auto with veto: the system acts and a dispatcher can undo within a window, gated on a low veto rate and outcome parity with the human baseline. Rung four, autonomous: the system acts and humans handle escalations only.">
<defs>
<marker id="III_l1" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse"><path d="M0,0 L10,5 L0,10 z" fill="currentColor"/></marker>
</defs>
<g font-family="'Source Sans 3', sans-serif" color="var(--line-2)">
<!-- rungs ascending -->
<rect x="0" y="300" width="250" height="140" rx="3" fill="var(--surface-2)" stroke="var(--line-2)"/>
<rect x="0" y="300" width="250" height="5" fill="var(--muted)"/>
<text x="20" y="330" font-family="'JetBrains Mono', monospace" font-size="10.5" letter-spacing=".1em" fill="var(--muted)">RUNG 01</text>
<text x="20" y="356" font-size="15" font-weight="600" fill="var(--ink)">Shadow</text>
<text x="20" y="380" font-size="11.5" fill="var(--ink-2)">runs on every booking,</text>
<text x="20" y="396" font-size="11.5" fill="var(--ink-2)">logs, changes nothing</text>
<text x="20" y="422" font-size="10.5" font-family="'JetBrains Mono', monospace" fill="var(--muted)">risk: zero</text>
<rect x="286" y="230" width="250" height="210" rx="3" fill="var(--surface-2)" stroke="var(--line-2)"/>
<rect x="286" y="230" width="250" height="5" fill="var(--gold)"/>
<text x="306" y="260" font-family="'JetBrains Mono', monospace" font-size="10.5" letter-spacing=".1em" fill="var(--gold)">RUNG 02</text>
<text x="306" y="286" font-size="15" font-weight="600" fill="var(--ink)">Advisory</text>
<text x="306" y="310" font-size="11.5" fill="var(--ink-2)">recommendation shown in</text>
<text x="306" y="326" font-size="11.5" fill="var(--ink-2)">the console; a dispatcher</text>
<text x="306" y="342" font-size="11.5" fill="var(--ink-2)">clicks to apply it</text>
<text x="306" y="368" font-size="10.5" font-family="'JetBrains Mono', monospace" fill="var(--muted)">risk: a wasted click</text>
<rect x="572" y="160" width="250" height="280" rx="3" fill="var(--gold-soft)" stroke="var(--gold)"/>
<rect x="572" y="160" width="250" height="5" fill="var(--gold)"/>
<text x="592" y="190" font-family="'JetBrains Mono', monospace" font-size="10.5" letter-spacing=".1em" fill="var(--gold)">RUNG 03</text>
<text x="592" y="216" font-size="15" font-weight="600" fill="var(--ink)">Auto with veto</text>
<text x="592" y="240" font-size="11.5" fill="var(--ink-2)">the system acts; a dispatcher</text>
<text x="592" y="256" font-size="11.5" fill="var(--ink-2)">can undo inside a window</text>
<text x="592" y="272" font-size="11.5" fill="var(--ink-2)">before the rider is notified</text>
<text x="592" y="298" font-size="10.5" font-family="'JetBrains Mono', monospace" fill="var(--muted)">risk: bounded &amp; reversible</text>
<rect x="858" y="90" width="250" height="350" rx="3" fill="var(--alt-soft)" stroke="var(--alt)" stroke-width="1.8"/>
<rect x="858" y="90" width="250" height="5" fill="var(--alt)"/>
<text x="878" y="120" font-family="'JetBrains Mono', monospace" font-size="10.5" letter-spacing=".1em" fill="var(--alt)">RUNG 04</text>
<text x="878" y="146" font-size="15" font-weight="600" fill="var(--ink)">Autonomous</text>
<text x="878" y="170" font-size="11.5" fill="var(--ink-2)">the system acts; humans</text>
<text x="878" y="186" font-size="11.5" fill="var(--ink-2)">handle escalations only</text>
<text x="878" y="212" font-size="10.5" font-family="'JetBrains Mono', monospace" fill="var(--muted)">risk: managed by §06</text>
<!-- gates -->
<line x1="254" y1="290" x2="282" y2="290" stroke="currentColor" stroke-width="1.5" marker-end="url(#III_l1)"/>
<rect x="150" y="252" width="256" height="30" rx="15" fill="var(--surface)" stroke="var(--ink)"/>
<text x="278" y="272" text-anchor="middle" font-size="10.5" font-family="'JetBrains Mono', monospace" fill="var(--ink)">gate: 2 000 logged + agreement measured</text>
<line x1="540" y1="220" x2="568" y2="220" stroke="currentColor" stroke-width="1.5" marker-end="url(#III_l1)"/>
<rect x="436" y="182" width="256" height="30" rx="15" fill="var(--surface)" stroke="var(--ink)"/>
<text x="564" y="202" text-anchor="middle" font-size="10.5" font-family="'JetBrains Mono', monospace" fill="var(--ink)">gate: acceptance &gt; 80% · 0 violations</text>
<line x1="826" y1="150" x2="854" y2="150" stroke="currentColor" stroke-width="1.5" marker-end="url(#III_l1)"/>
<rect x="722" y="112" width="256" height="30" rx="15" fill="var(--surface)" stroke="var(--ink)"/>
<text x="850" y="132" text-anchor="middle" font-size="10.5" font-family="'JetBrains Mono', monospace" fill="var(--ink)">gate: veto &lt; 5% · outcome parity</text>
<line x1="0" y1="458" x2="1108" y2="458" stroke="var(--line)" stroke-width="1"/>
<text x="0" y="476" font-size="11.5" fill="var(--muted)">Any capability can sit on a different rung. Address resolution may be autonomous while assignment is still advisory — that is the point of separating them.</text>
</g>
</svg>
</div>
<figcaption><b>Figure III.2 — four rungs, three numeric gates</b>Shadow mode is free and tells you more than any amount of design discussion. Run every new capability there first — including the ones you are confident about, because that is where the surprises are.</figcaption>
</figure>
</section>
<section id="loop">
<div class="sec-head"><span class="sec-num">05</span><div><h2>The loop you already built</h2></div></div>
<p class="measure">This is the sharpest finding in the codebase, and it makes everything above cheaper.</p>
<figure>
<div class="fig-scroll">
<svg viewBox="0 0 1120 420" role="img" aria-label="The AgentDecision feedback loop. Solid lines show what exists: the assignment writes context, decision and reasoning into an AgentDecision row, and an internal PATCH endpoint for recording an outcome is already routed and implemented. A dashed line shows the one missing connection: nothing in the deliver or skip handlers calls that endpoint, so the outcome and outcome-recorded-at columns stay empty and no decision is ever scored.">
<defs>
<marker id="III_m1" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse"><path d="M0,0 L10,5 L0,10 z" fill="currentColor"/></marker>
<marker id="III_m1a" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse"><path d="M0,0 L10,5 L0,10 z" fill="var(--accent)"/></marker>
</defs>
<g font-family="'Source Sans 3', sans-serif" color="var(--line-2)">
<rect x="0" y="60" width="250" height="80" rx="3" fill="var(--surface-2)" stroke="var(--line-2)"/>
<text x="125" y="92" text-anchor="middle" font-size="12.5" font-weight="600" fill="var(--ink)">assignment made</text>
<text x="125" y="112" text-anchor="middle" font-size="10.5" font-family="'JetBrains Mono', monospace" fill="var(--muted)">selectMilerWithAI</text>
<line x1="254" y1="100" x2="300" y2="100" stroke="currentColor" stroke-width="1.5" marker-end="url(#III_m1)"/>
<rect x="304" y="40" width="330" height="120" rx="3" fill="var(--surface)" stroke="var(--ink)" stroke-width="1.8"/>
<text x="469" y="68" text-anchor="middle" font-size="13" font-weight="600" fill="var(--ink)">AgentDecision row</text>
<text x="322" y="92" font-size="11" font-family="'JetBrains Mono', monospace" fill="var(--alt)">✓ Context jsonb</text>
<text x="322" y="110" font-size="11" font-family="'JetBrains Mono', monospace" fill="var(--alt)">✓ Decision jsonb</text>
<text x="322" y="128" font-size="11" font-family="'JetBrains Mono', monospace" fill="var(--alt)">✓ Reasoning text</text>
<text x="322" y="148" font-size="11" font-family="'JetBrains Mono', monospace" fill="var(--accent-ink)">✗ Outcome always null</text>
<line x1="638" y1="100" x2="684" y2="100" stroke="currentColor" stroke-width="1.5" marker-end="url(#III_m1)"/>
<rect x="688" y="60" width="392" height="80" rx="3" fill="var(--surface-2)" stroke="var(--line-2)"/>
<text x="884" y="88" text-anchor="middle" font-size="12.5" font-weight="600" fill="var(--ink)">PATCH /internal/agent-decisions/:id/outcome</text>
<text x="884" y="110" text-anchor="middle" font-size="11" fill="var(--alt)">routed · implemented · working</text>
<text x="884" y="128" text-anchor="middle" font-size="11" font-weight="600" fill="var(--accent-ink)">called by nothing</text>
<!-- the missing wire -->
<rect x="0" y="266" width="250" height="80" rx="3" fill="var(--surface-2)" stroke="var(--line-2)"/>
<text x="125" y="298" text-anchor="middle" font-size="12.5" font-weight="600" fill="var(--ink)">parcel resolves</text>
<text x="125" y="318" text-anchor="middle" font-size="10.5" font-family="'JetBrains Mono', monospace" fill="var(--muted)">deliver · skip · RTO</text>
<path d="M250,306 H884 V148" fill="none" stroke="var(--accent)" stroke-width="2" stroke-dasharray="7 5" marker-end="url(#III_m1a)"/>
<rect x="380" y="292" width="330" height="30" rx="15" fill="var(--accent-soft)" stroke="var(--accent)"/>
<text x="545" y="312" text-anchor="middle" font-size="12" font-weight="600" fill="var(--accent-ink)">the one wire that is missing</text>
<text x="0" y="386" font-size="12.5" fill="var(--ink-2)">Column, index, endpoint and route all exist. One call from the resolution handlers turns every past assignment into</text>
<text x="0" y="406" font-size="12.5" fill="var(--ink-2)">a labelled training example — and gives you the replay corpus every gate in §04 is measured against.</text>
</g>
</svg>
</div>
<figcaption><b>Figure III.3 — 90% built, 0% connected</b><code>AgentDecision.Outcome</code>, <code>OutcomeRecordedAt</code>, the controller and the route are all in place. Nothing writes to them. This is the cheapest high-value change available in the whole system.</figcaption>
</figure>
<div class="callout">
<span class="label">Do this in week one</span>
<p>Call <code>UpdateDecisionOutcome</code> from the deliver, skip and RTO handlers with a simple verdict — delivered first attempt, delivered late, failed, reassigned. From that day forward every dispatch decision is scored, you can replay history against any new policy offline, and every claim anyone makes about the AI dispatcher becomes checkable. Until then, no gate in §04 can be measured at all.</p>
</div>
</section>
<section id="guard">
<div class="sec-head"><span class="sec-num">06</span><div><h2>Guardrails</h2></div></div>
<ul class="plain">
<li><b>Closed-set selection, enforced server-side.</b> A model returning <code>chosen_miler_id</code> must have that ID checked against the candidate set before commit. Models hallucinate identifiers; a validator costs three lines and removes the entire failure class.</li>
<li><b>Keep the payload structured.</b> Your current request to <code>routemate</code> carries no customer free text. The moment you pass raw <code>Notes</code>, address strings or client emails into a prompt that also drives actions, you have built a prompt-injection channel — anyone who can type into a booking form can try to instruct your dispatcher. If unstructured text must be read, do it in a separate call that only <em>extracts</em>, and never let that call's output reach an action without validation.</li>
<li><b>Tenant isolation is an invariant, not a preference.</b> <code>HubStaffAccount.Tenantid</code> already isolates partner freight. Agents must be scoped identically, and cross-tenant leakage should be a hard validator failure, not a scoring penalty. This is the one mistake that is a breach rather than a bad route.</li>
<li><b>Every agent action is idempotent.</b> You already have the Redis-backed key infrastructure. Agents retry more than humans do; a duplicated reassignment or a double COD entry is far worse than a slow one.</li>
<li><b>Budget per booking, enforced.</b> Set a token ceiling per decision and per day, with a hard cutoff to the deterministic path. Cost overruns in agentic systems come from retry storms, not from steady state.</li>
<li><b>Mind what leaves the estate.</b> Consignee phone numbers, COD amounts and precise home coordinates are in scope for whatever model endpoint you call. Decide deliberately what is sent, and prefer IDs over identities wherever the model does not need the human detail.</li>
<li><b>Escalate rather than improvise.</b> Give the exception agent an explicit "page a human" tool and reward using it. An agent with no honourable way to stop will invent an action instead.</li>
</ul>
</section>
<section id="plan">
<div class="sec-head"><span class="sec-num">07</span><div><h2>Ninety days</h2></div></div>
<div class="table-scroll">
<table>
<thead><tr><th>Phase</th><th>Ship</th><th>Rung</th><th>Proves</th></tr></thead>
<tbody>
<tr><td class="num">Wk 1–2</td><td>Wire <code>UpdateDecisionOutcome</code> from deliver / skip / RTO. Build the replay harness over <code>AgentDecision</code>.</td><td>—</td><td>Every later claim becomes measurable.</td></tr>
<tr><td class="num">Wk 3–4</td><td>Real ETA: Valhalla road time × time-of-day factor + learned dwell, replacing <code>calculateETA</code>.</td><td>direct</td><td>No model needed; immediate accuracy win on all three legs.</td></tr>
<tr><td class="num">Wk 3–6</td><td>Address resolution on pgvector. Embed delivered addresses, cluster, snap to <code>DeliveryProof</code> coordinates.</td><td>shadow → advisory</td><td>Highest-leverage capability, lowest risk.</td></tr>
<tr><td class="num">Wk 5–8</td><td>VRP solver behind a flag: batched joint assign + sequence, running in shadow beside today's path.</td><td>shadow</td><td>Head-to-head on the replay corpus against live decisions.</td></tr>
<tr><td class="num">Wk 7–9</td><td>Explainer on solver output; dispatcher and rider-facing, in-language.</td><td>advisory</td><td>Adoption. Riders follow routes they understand.</td></tr>
<tr><td class="num">Wk 9–11</td><td>NL constraint compiler with a human approval queue.</td><td>advisory</td><td>Tacit dispatcher knowledge starts reaching the solver.</td></tr>
<tr><td class="num">Wk 10–12</td><td>Exception agent with a bounded tool set, proposals only.</td><td>advisory</td><td>The escalation queue starts shrinking.</td></tr>
<tr><td class="num">Wk 12</td><td>Promote whichever capabilities have cleared their gates. Not the ones that haven't.</td><td>→ auto with veto</td><td>The ladder does the deciding, not the roadmap.</td></tr>
</tbody>
</table>
</div>
<div class="callout teal">
<span class="label">What is deliberately not in the first ninety days</span>
<p>Vision on inbound condition and POD, vernacular voice, and the operator copilot's action tier are all worth building — and all of them are easier once the outcome loop, the replay harness and the validator exist. Building those three foundations first is what makes the rest take weeks instead of quarters.</p>
</div>
</section>
<footer>
Read against <code>DOORMILE_LOGISTICS_ARCHITECTURE.md</code> and the <code>doormile_backend</code>, <code>doormile_express_console</code> and <code>backend_jupiter</code> sources in the nearle-de workspace &mdash; including <code>constants/constants.go</code>, <code>internal/assignment/</code>, <code>internal/routing/optimizer.go</code>, <code>models/audit.go</code>, <code>models/transit.go</code>, <code>models/agentdecision.go</code> and <code>controllers/</code>. Status strings, constants, endpoints and formulas are quoted as they appear there. Section and figure numbers restart within each part.
</footer>
</main>
</div>
</div>
</body>
</html>