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.