Routing
Sep 05, 2023214 min read

Route Treaties: Deep Dive #74

AV
Ariel Voss

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

  1. Treaties are negotiated in the shell (policy owner).
  2. Terms are deterministic checks with explicit ok/fail results.
  3. Treaty grants are shaped (normalized params, granted track, fallback route id).
  4. Every term emits transcript evidence.

Worked Example: treaty:invoice (normalize + terms + grant + fallback)

We’ll define a treaty for our Invoice Explorer:

  • invoiceId is always normalized to a string
  • detail track 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:

  1. Normalize input (make params shape stable).
  2. Evaluate deterministic terms (ok/failed).
  3. Write evidence for each term (so failures are inspectable).
  4. Shape the final grant object (so callers don’t re-infer meaning).
  5. 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 fallbackRouteId and writing a treaty.grant row with fallback.
  • 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).