# Design principles

> The fourteen rules Carte's code holds itself to, and why.

Carte's design was written down before it was extracted from Masque, and each rule below is held by the code, usually by a type signature or a test rather than a comment. When the code and a rule disagree, that is treated as a bug in one of them, never something to paper over.

## Everywhere

### 1. Depends on nothing, draws nothing

Carte's `[dependencies]` table is empty, and `tests/independence.rs` fails the build if anything is added. It names no UI type, so any UI can sit on top: immediate-mode, retained, or a UI language. Your binding is the only code that knows both sides.

### 2. Fail at startup, naming the culprit

A typo'd parent, a cycle, a scope keyed on an unregistered id, a second guard on one node, two producers claiming one field: each is an error from `RouteTable::validate`, `Router::new`, `Store::provides` or `Store::producer`, before the first frame. Every error names the id, scope or field at fault. The alternative is a wrong screen, found by a user.

## Routing

### 3. The path is derived, never pushed

There is no navigation stack. Every frame, the walk asks each node on the path which child it is showing, starting from your root function. Navigating means changing your state. Even a pop does not edit the path: it asks the parent to stop claiming its child, and the next walk is shorter.

### 4. A node claims at most one child

`resolve_child` returns `Option<RouteId>`. A node cannot claim two children, so there is no arbiter and no exclusion list, and the walk always ends. Several overlays that must never show together become one `Option` instead of several booleans and a rule.

### 5. Enter and leave mean joining and leaving the path

`enter` runs when an id joins the path and `leave` when it leaves. Not when it becomes or stops being the deepest. A prompt opened over an editor does not make the editor leave, so the editor's draft survives its own exit prompt.

### 6. A node guards its own door

A precondition lives with the node it protects, as a plain Rust function over the store. The walk asks it before stepping in, and `Router::ask` asks the same function for a greyed-out button. A refusal is one sentence, written once, and every surface shows that sentence.

### 7. Screens can only ask

Node methods that run during a frame get `&` your model and an `&mut Outbox`. Views get the committed store and the outbox. The only way to change anything is an action, applied in one place, in order. This is checked by the compiler, not by convention.

## State

### 8. A scope lives exactly as long as its node is on the path

The walk mounts a node's scope when the node joins the path and drops it when it leaves. Nothing else can: mounting is not public. State that outlives its screen cannot happen by accident. If you want state to outlive a screen, you put it somewhere that does.

### 9. One frame, one commit

`Store::tick` drains the outbox, runs producers, and commits, in that order, and the signature makes any other order impossible to write. Views never see a half-written frame, an action asked for this frame shows this frame, and ten sets produce one notice.

### 10. One writer per field

A producer claims the fields it writes, and a field claimed twice is a startup error. Writing a field you did not claim is refused. A fact with two writers looks right on the frames each one wins, which is why it has to fail early.

### 11. Dirtiness comes from values, not flags

Every set is compared with the current value, and a set that changes nothing is dropped. Producers write everything every tick without tracking changes. What woke a subscriber is what actually changed.

### 12. The declaration is the starting value

`Store::provides` takes a type and no value. A scope starts at the values its schema declares, and the schema records whether a value was chosen or fell through to zero. There is no second value to disagree with the declaration.

### 13. Facts, not presentation

The store holds facts about what should be shown: a title, a flag, a selected id. Not vertex buffers, not eased values, not ten thousand rows. A scope is cloned on every commit that touched it, and field lookups refuse to step into a collection, so leaning on the store for bulk data is uncomfortable on purpose.

## Contract

### 14. Validation is structural, and comes in two grades

Shapes are compared by field names and kinds, never by type names or field order. A view that expects something missing **refuses**, loudly, naming the field. Coverage drift (a field nobody reads, an intent nobody raises) only **reports**, because a build that refused it would be a build nobody could run, and a check nobody can run gets deleted.

TODO: link each principle to its section in the design record once that record is published.
