One command, every layer.
The fan-out rule says a schema change must be handled by every active producer and
consumer. Here is what that means in practice: the full life of one
trading.mission.risk-control press, from a workspace button to
the correlated result event.
press · apps/web
The button does not trust its own dispatch
useMissionControls binds one dispatcher per control and
keeps the press pending until a durable result arrives. Its module doc states the rule
as RC06: "a dispatched command only proves the request was accepted" (apps/web/src/components/trading/useMissionControls.ts:14, the hook at :126, the risk dispatcher at
:131). §14.7 defines these controls so a workspace button
invokes them directly, with no harness turn and no dependency on the bound provider
being online (packages/contracts/src/trading.ts:1226-1235).
mint · client-runtime
A typed command is born
tradingRiskControl mints a fresh
commandId and createdAt, then
dispatches the trading.mission.risk-control member with the
mission, thread, control, and an optional
reductionPercent for
reduce_position (packages/client-runtime/src/operations/commands.ts:449-462). The command struct itself is a schema (packages/contracts/src/trading.ts:1236-1245).
wire · packages/contracts
It crosses as a union member, and comes back as a number
The command travels inside
ClientOrchestrationCommand through
WsOrchestrationDispatchCommandRpc. The success reply is a
DispatchResult led by
sequence (packages/contracts/src/rpc.ts:906-913). At this point the client knows only that the request landed in the ordered stream.
decide · apps/server
The decider asks, and refuses to answer
The pure decider maps the command to a
trading.mission-risk-control-requested event. It
deliberately resolves no target status here, because "what the mission becomes depends
on what the exchange does, so the reactor decides it after the fact" (apps/server/src/orchestration/decider.ts:1475-1499). The event is persisted before any side effect runs.
act · apps/server
The reactor does the exchange work, then reports
TradingMissionReactor answers the question. Its module doc:
the decider's *-requested event "is a question, not an
answer", and the ordering is the whole point, because "The UI never sees a status the
domain refused" (apps/server/src/trading/TradingMissionReactor.ts:4-10). When the exchange work completes, fails, or cannot be confirmed, it raises the
internal trading.mission.control-result command.
correlate · every client
The result names the press that caused it
The control-result payload carries requestEventSequence: the
sequence of the requested event the reactor answered, "so a client can correlate the
result to the press that caused it and ignore results belonging to another request"
(packages/contracts/src/trading.ts:1247-1257, payload at
:1628-1645). Status is one of
completed, failed, or
unknown, because an exchange that timed out is an honest
state, not a guess.
same code · apps/desktop
Desktop inherits the whole path
The desktop shell loads the web app in its window (apps/desktop/src/window/DesktopWindow.ts:623) and builds on the same @t3tools/client-runtime. There is
no second trading producer to update, which is exactly why the fan-out rule matters:
one contract edit reaches every file on this timeline in one rebuild, and misses none
of them.