Skip to content

Mutations are fully addressed

State: open/published

  • A mutation API names its target in full, in its type: not by convention, and not by a leading positional argument reassembled downstream.
  • An ambient or implicit target is legitimate in the UX layer: a selected vault, a focused window, a current project. It is a default the interface supplies, never an answer the API assumes.
  • An un-addressed mutation is a defect with one known fix, not a mode to declare or a state to report at runtime.
  • Enforcement belongs at the boundary that already knows the shape (a codegen or contract gate), not in a hand-maintained manifest.

A mutation carries its target in full. The thing it changes is a required field of its own request type, so a caller that does not know what it is changing cannot compile. Not a convention about which argument comes first. Not a value the process happens to be holding when the call arrives.

An implicit target is a real and useful thing, and it belongs one layer up. A selected vault, a focused window, a current project, a checked-out branch: each is what the user means when they do not say. The interface resolves it and passes it. The API never resolves it, because the API cannot tell a caller that omitted the target from a caller that never had one.

A read and a write fail differently. A read against the wrong target is on screen: the user sees the wrong list and corrects it. A write against the wrong target is silent, and the user finds out later, if at all. So a write cannot borrow a read’s tolerance for an implicit target. A read that cannot fan out is a product limitation; a write that does not name its target is a defect.

The alternative that keeps getting built is a compensating control: a probe, an audit, a declared field that reports at runtime where a write is expected to land. That is what a missing type looks like from the outside. A probe can inspect a calling convention but never enforce one. It grades what already shipped, it is wrong whenever the convention changes ahead of it, and it drifts because nothing breaks when it does. The type is cheaper and the compiler checks it.

  • The target is a required, non-nullable field on the request type. An optional field with a fallback is the same defect wearing a type.
  • The client resolves the ambient default and passes it explicitly. “The user is looking at X” is a fact the client has and the server does not.
  • An operation with genuinely no target (scoped to the machine, the process, or the roster itself rather than to any addressable thing) is an exception. It is named in one place, with a written reason. The reason is what separates an exception from an oversight.
  • Enforcement rides a boundary that already parses the shape. Where a transport, a schema, or a client is generated, the generated artifact is what the gate reads. A hand-maintained list of compliant endpoints is a second thing to keep true, and it will not be kept true.
  • Addressing is per request. Whether one process can serve several targets at once is a separate concurrency question, answered in the process’s own design. Confusing the two stalls the cheap fix behind the expensive one.
  • determined’s platform transport. Fifteen mutation endpoints resolved against one ambient active-vault pointer, so a save issued from a window showing vault B landed in vault A. Each request struct gains a required vault field, the frontend passes the vault the view is showing, and a test over the generated transport fails the build for any mutation without that field. Adopted for that transport in determined’s decision D-0017.
  • The exception list is part of the gate. Machine-level and process-level operations (ensuring a model is downloaded, starting a supervised daemon, listing the roster) take no target. They sit on a commented allowlist inside the gate itself, each with its reason, so adding an entry is a reviewable act rather than a silent omission.
  • This principle has a prerequisite it does not supply. A required target field is worth exactly as much as the identity it carries. If an id means different things in different processes, or does not survive the thing it names being moved, the field buys correctness within one process and nothing beyond it. Stable identity is prerequisite work and should be named as such.
  • It does not govern reads. Widening a read is a product decision about fan-out, cost, and honesty, and “never widened” is a legitimate end state for most reads.
  • It applies at the moment a mutation is added, where it is cheapest. The gate makes that automatic; retrofitting existing endpoints is a finite, enumerable project rather than an ongoing discipline.
  • A related gap it exposes and does not close: naming the target does not stop a caller passing one target’s id beside another target’s entity name when both are strings. That is a typed-identity question and deserves its own Decision.

This is P-0005-schema-over-prose applied to one thing that keeps being expressed as prose. A declared write-target, a probe verdict, and a runtime banner are three ways of describing a fact the type could have carried. Model it, and the description has nothing left to say.


← Back to Principles