Route Treaties: Deep Dive #74
Principal React Architect
Choreography is the sequence. Transcripts are the evidence. Switchbacks are the rewind.
Treaties are what make all three safe to scale across a team.
As soon as more than one person touches routing, you get “silent policy”:
- one route normalizes params, another assumes they’re normalized,
- a third hydrates draft state in a leaf component “because it was convenient”,
- and the shell starts granting transitions based on inconsistent surfaces.
A route treaty is the antidote. It’s a contract between a route module and the shell that answers:
- what a route may request,
- what the shell may grant,
- what evidence must be written,
- and which fallback route applies when terms fail.
Required docs links:
Extra docs links for this entry:
Why treaties exist (the team problem)
Without treaties, “routing behavior” becomes an accidental API:
- one route normalizes params,
- another assumes params are already normalized,
- a third hydrates draft state in a leaf component,
- and the shell ends up granting transitions based on inconsistent surfaces.
Treaties fix this by making routing a predictable interface:
- routes request a treaty, not a raw transition,
- the shell negotiates the treaty,
- and the transcript shows exactly which terms passed or failed.
Treaty doctrine
- Treaties are negotiated in the shell (policy owner).
- Terms are deterministic checks with explicit ok/fail results.
- Treaty grants are shaped (normalized params, granted track, fallback route id).
- Every term emits transcript evidence.
Worked Example: treaty:invoice (normalize + terms + grant + fallback)
We’ll define a treaty for our Invoice Explorer:
invoiceIdis always normalized to a stringdetailtrack requires a draft (shell-hydrated)- failure falls back to
file:/invoices
Step 1: Types
export type TreatyTerm =
| { ok: true; id: string; meta?: Record<string, string | number | boolean> }
| { ok: false; id: string; reason: string; meta?: Record<string, string | number | boolean> };
export type TreatyGrant = {
normalizedParams: Record<string, string>;
grantedTrack: "summary" | "detail";
fallbackRouteId: string | null;
};
export type RouteTreaty = {
id: string;
normalize: (intent: any) => Record<string, string>;
decideTrack: (intent: any) => "summary" | "detail";
terms: Array<(intent: any, shell: any) => TreatyTerm>;
fallback: (intent: any, failed: TreatyTerm[]) => string;
};
Step 2: Treaty definition
export function defineInvoiceTreaty(): RouteTreaty {
return {
id: "treaty:invoice",
normalize: (intent) => ({ invoiceId: String(intent.params.invoiceId ?? "") }),
decideTrack: (intent) => intent.requestedTrack,
terms: [
(intent) => {
const ok = Boolean(intent.params.invoiceId);
return ok
? { ok: true, id: "term.invoiceId.present" }
: { ok: false, id: "term.invoiceId.present", reason: "missing" };
},
(intent, shell) => {
if (intent.requestedTrack !== "detail") {
return { ok: true, id: "term.detail.requires.draft", meta: { skipped: true } };
}
const ok = Boolean(shell.read("draft"));
return ok
? { ok: true, id: "term.detail.requires.draft" }
: { ok: false, id: "term.detail.requires.draft", reason: "draft-not-hydrated" };
},
],
fallback: (_intent, failed) => {
const first = failed.find((t) => t.ok === false) as any;
return first?.id === "term.invoiceId.present" ? "file:/invoices" : "file:/";
},
};
}
Step 3: Negotiation (write term evidence + grant evidence)
Treaty negotiation should always be transcripted. That’s how you make treaty failures debuggable and testable.
export function negotiateTreaty(args: {
shell: any;
transcript: ReturnType<typeof createRouteTranscript>;
intent: any;
treaty: RouteTreaty;
}) {
const { shell, transcript, intent, treaty } = args;
const normalizedParams = treaty.normalize(intent);
const normalizedIntent = { ...intent, params: normalizedParams };
transcriptRequest({
transcript,
intentId: intent.intentId,
routeId: intent.routeId,
track: intent.requestedTrack,
stepId: "treaty.request",
requested: treaty.id,
cause: intent.cause,
surface: "route",
});
const results = treaty.terms.map((t) => t(normalizedIntent, shell));
const failed = results.filter((r) => r.ok === false) as any[];
results.forEach((r) => {
transcriptGrant({
transcript,
intentId: intent.intentId,
routeId: intent.routeId,
track: intent.requestedTrack,
stepId: r.id,
granted: r.ok ? "ok" : "failed",
cause: "shell:treaty-eval",
surface: "shell",
meta: r.ok ? r.meta : { ...r.meta, reason: r.reason },
});
});
const grant: TreatyGrant = {
normalizedParams,
grantedTrack: treaty.decideTrack(normalizedIntent),
fallbackRouteId: failed.length ? treaty.fallback(normalizedIntent, failed) : null,
};
transcriptGrant({
transcript,
intentId: intent.intentId,
routeId: intent.routeId,
track: intent.requestedTrack,
stepId: "treaty.grant",
granted: grant.fallbackRouteId ? "fallback" : "granted",
cause: "shell:treaty-grant",
surface: "shell",
meta: grant,
});
return grant;
}
Step 3b: What "negotiation" actually does
Treaty negotiation is deliberately repetitive:
- Normalize input (make params shape stable).
- Evaluate deterministic terms (ok/failed).
- Write evidence for each term (so failures are inspectable).
- Shape the final grant object (so callers don’t re-infer meaning).
- Apply the grant in the shell (so leaf routes stay policy-free).
This repetition is the point: treaties are a team-scale API, and boring beats clever.
Step 4: Apply grant (or fallback) in the shell
import { useLiveState } from "react";
export function InvoiceTreatyShell() {
const shell = useLiveState("invoice.shell", {
routeId: "file:/",
track: "summary",
params: {},
draft: null,
transcript: [],
});
const transcript = createRouteTranscript(shell);
const treaty = defineInvoiceTreaty();
function request(intent: any) {
const grant = negotiateTreaty({ shell, transcript, intent, treaty });
if (grant.fallbackRouteId) {
shell.write("routeId", grant.fallbackRouteId);
shell.write("track", "summary");
shell.write("params", {});
return;
}
shell.write("routeId", intent.routeId);
shell.write("track", grant.grantedTrack);
shell.write("params", grant.normalizedParams);
}
return (
<section data-shell="invoice-treaty">
<h1>Invoice Treaty</h1>
<div className="mt-4 flex flex-wrap gap-3">
<button
onClick={() =>
request({
intentId: "intent:ok",
routeId: "file:/invoices/[invoiceId]",
params: { invoiceId: "92" },
requestedTrack: "detail",
cause: "ui:open-detail",
})
}
>
Request detail (ok)
</button>
<button
onClick={() =>
request({
intentId: "intent:bad",
routeId: "file:/invoices/[invoiceId]",
params: {},
requestedTrack: "detail",
cause: "ui:open-detail",
})
}
>
Request detail (missing invoiceId)
</button>
</div>
<RouteTranscriptPanel shell={shell} />
</section>
);
}
Common failure modes (treaty-specific)
- Hidden normalization: a route silently normalizes params in a leaf helper and the shell never sees it — fix by putting
normalize()in the treaty and transcript the grant meta. - Implicit fallback: code "just renders something else" when terms fail — fix by always returning an explicit
fallbackRouteIdand writing atreaty.grantrow withfallback. - Term drift: teams add new terms in one module but forget to update tests — fix by asserting term ids from the transcript DOM, not from internal functions.
Checklist
- Routes request treaties (not raw transitions).
- Shell negotiates treaties (policy owner).
- Every term writes transcript evidence.
- Grants are shaped (normalized params + granted track + fallback).