# Validation

> Checking what your views read and raise against what the store publishes, in two grades.

The store knows what every scope provides and accepts. Your views know what they read and raise. Validation puts the two side by side and grades every disagreement, so a view that reads a field nobody provides fails a test instead of drawing an empty label.

## Two grades

Every finding either **refuses** or **reports**.

- **Refuses** means a view expects something that is not there. It reads a field no scope provides, or raises an intent no scope accepts. Left alone, this is a screen that draws perfectly and shows nothing, or a button that reaches nobody. It should be loud.
- **Reports** means coverage drift: a field nothing reads, an intent nothing raises, a route no view draws. That is usually a developer part-way through a change, or a scope deliberately publishing more than the shipped views use. It should be visible, but it should not stop a build.

The grades travel together. A run that refuses one stale field also lists the four fields nobody reads.

## What Carte does and does not do

Carte has no idea what your views are. It reads no files and parses nothing. A "document" here is whatever declares what a view reads: a template, a view module, a file in a UI language. You describe each one to Carte as a list of field names and raises.

So validation needs a **harness**: a test in your app that walks your view definitions, turns each into those lists, and hands them over. With a UI language that can report its own selections, the harness is mechanical. With hand-written Rust views, you list them yourself.

> TODO: no ready-made harness ships for any UI layer yet, including egui and Masque.

## Running a check

```rust
use carte::{Declared, Kind, Scope, Store, Validation};

#[test]
fn views_match_the_store() {
    let store = build_store().unwrap(); // your app's setup
    let mut check = Validation::new(&store);

    let note = [EDITOR, Scope::Process]; // nearest first
    check.selects_named("note", &note, &["title".into(), "words".into()]);
    check.raises("note", &note, &[Declared::new("rename", vec![Kind::Str])]);
    check.draws("ui", &["home", "note"], table().ids());

    let findings = check.finish();
    assert!(!findings.refuses(), "{findings}");
}
```

Each call names a document and the scopes it reads from, **nearest first**: the view's own scope, then whatever encloses it, then `Scope::Process`. Which scopes those are is an input. Carte does not work it out for you.

| Method | Checks |
| --- | --- |
| `selects_named(doc, scopes, names)` | Each name is a field some scope provides |
| `selects(doc, scopes, fields)` | The same, and that each `Field`'s shape matches the provider's |
| `reads(doc, scopes, paths)` | Each dotted path such as `"card.title"`, where `card` is a record field, reaches a real field |
| `raises(doc, scopes, declared)` | Each raise is accepted, with the right arguments |
| `draws(doc, declared, registered)` | The document's screens against the route ids it should draw |
| `unmounted(doc, scope_name, scopes)` | Records a read from a scope the document is not under |
| `publishes(scope)` | Whether the store publishes anything for a scope |

`finish()` adds the findings that are about the whole set rather than one document: fields nothing read and intents nothing raised. It returns `Findings`.

## Findings

| Finding | Grade | Means |
| --- | --- | --- |
| `Unprovided` | Refuses | A selected field no scope provides |
| `Shape` | Refuses | A field provided with a different shape |
| `Unreached` | Refuses | A dotted read that names a missing field |
| `Unaccepted` | Refuses | A raise no scope accepts |
| `Raise` | Depends on the `Drift` | A raise with wrong arguments, or similar |
| `Unpublished` | Refuses | A scope in the list that publishes nothing |
| `Unmounted` | Refuses | A read from a scope the document is not under |
| `Shadowed` | Reports | Two scopes provide the field; the nearer one wins |
| `Unread` | Reports | A provided field nothing selects |
| `Unraised` | Reports | An accepted intent nothing raises |
| `Undrawn` | Reports | A route the document draws no screen for |
| `Unrouted` | Reports | A screen no route reaches |

Each finding names its document, field, intent or screen. `Finding::refuses()` gives its grade, and `Finding::document()` the document it is about, if any. `Findings` has `all()`, `refusals()`, `reports()`, `refuses()` and `is_empty()`, and prints grouped under `refuses:` and `reports:` headings.

## What it cannot see

Validation refuses only what it can see. A path into a list, a map or a union cannot be followed past that point, because a collection is one field. A read through a helper's parameter or a computed key is invisible. Both are silent, not reported. A refusal that a correct view cannot avoid is a check people switch off.

Before naming a node in a scope list, ask `publishes(scope)`. Most view nodes own no scope, and naming one that publishes nothing is an `Unpublished` refusal.

## Next

- [Design principles](/docs/principles): why the grades are drawn where they are.
- [Status and roadmap](/docs/roadmap): what is still missing.
