# Contract: a complete working guide for agents > **Lookup reference: start at [Start here](start-here.md).** Building an app needs > only that page, and a [recipe](recipes/) answers most questions after it. Open this > guide at a section for what no recipe covers, or grep it; don't read it front to back. Use this guide to author, change, inspect, and verify a current Exact application. It covers the language implemented on `main` on 2026-10-02. Use the [human guide](contract-for-humans.md) for explanations and complete examples, and the [grammar reference](contract-grammar.md) for exact forms, built-in functions, tags, and event payloads. The compiler and executable fixtures are authoritative. Read the repository's `AGENTS.md`, `rules/RULES.md`, and `rules/DEFERRED.md` before making changes here. This guide is documentation, not an additional policy layer. Design documents from Exact's predecessor describe that predecessor and do not establish current capabilities. ## Contents - [Orient and choose the right layer](#orient-and-choose-the-right-layer) - [The implementation loop](#the-implementation-loop) - [Language inventory](#language-inventory) - [Values, expressions, and functions](#values-expressions-and-functions) - [State and action semantics](#state-and-action-semantics) - [Composition and lifetime](#composition-and-lifetime) - [Data requests and side effects](#data-requests-and-side-effects) - [Native sign-in](#native-sign-in-exactsign-in) - [Views, layout, and interaction](#views-layout-and-interaction) - [Tabular numbers](#tabular-numbers) - [Sound, MIDI and media](#sound-midi-and-media) - [Routes and web documents](#routes-and-web-documents) - [Tabs and stacks](#tabs-and-stacks) - [Time, motion, graphics, and platform facts](#time-motion-graphics-and-platform-facts) - [Inspection and testing](#inspection-and-testing) - [Repair common mistakes](#repair-common-mistakes) - [Completion criteria](#completion-criteria) ## Orient and choose the right layer 1. Identify the app, its root `app.contract`, `app.json`, data module, imports, authored tests, and requested target hosts. An external app uses `EXACT_APP_DIR` with the same exact2 build/driver scripts. 2. Read the nearest working example for the feature. Search `contract/corpus/`, `contract/cli/tests/it/`, and the app's sources before designing new syntax. 3. Decide whether the change belongs in Contract, the app's data module, its optional native/GPU module, or the host. Do not extend the compiler to avoid writing an ordinary data-source function. 4. Check the current branch and dirty files. Preserve other work. Do not commit unrelated staged paths or use `git stash` in this repository. | Work | Normal home | | --- | --- | | View structure, UI state, event binding | `.contract` | | Derived labels, record copies, bounded list transforms, a presentation order by one key (`sortBy`) | Contract expressions / `fn` | | Network, authentication, storage, a stored or locale order, domain algorithms | App TypeScript/Rust data module | | Device facts | Reserved source with an admitted shape | | A system control (button, list, switch, picker, menu, tabs, bars) | Contract's native form ([below](#views-layout-and-interaction)); an app native module only where none exists | | What only the platform's own object can do (a gesture recognizer, a bar's look, drawing into a view) | An access hatch: `hatch="word"` on the node, native code handed its view ([Exact developer reference](reference.md#access-hatches)); `bun exact.mjs hatch ` writes the stubs | | Canvas 2D drawing | Data module's canvas surface | | GPU scene or game | Optional GPU/game artifact | | App identity, grants/deploy selection, module placement | App manifest and data-module declarations | Windows apps default to ink, or CPU when the manifest declares `game`; `host.windows.painter: "ink"` or `"cpu"` selects either, and `EXACT_PAINTER` overrides both. Headless drives explicitly use CPU unless overridden, so inspect the desktop capture too. See the [Windows painter choices](reference.md#windows-painter) for device selection and the other platforms' defaults. For the implementation, the relevant authorities are: | Question | Source | | --- | --- | | Tokens, indentation, escapes | [`lexer.rs`](../contract/syntax/src/lexer.rs) | | Declarations, statements, elements | [`parser.rs`](../contract/syntax/src/parser.rs) | | Precedence, templates, callbacks | [`parser/expr.rs`](../contract/syntax/src/parser/expr.rs) | | Records, locals, callbacks | [`types/records.rs`](../contract/types/src/records.rs), [`types/actions.rs`](../contract/types/src/actions.rs), [`types/lists.rs`](../contract/types/src/lists.rs) | | Tags, aliases, authored CSS names | [`lower/tags.rs`](../contract/lower/src/tags.rs) | | Kernel property declarations | [`schema.json`](../kernel/tables/schema.json) | | Function signatures | [`format.json`](../plan/tables/format.json), field `stdlib`, plus compiler intrinsics | | Commands and argument checks | [`types/checks.rs`](../contract/types/src/checks.rs) | | Event payloads and arities | [`analyze/lib.rs`](../contract/analyze/src/lib.rs) | | Imports and file boundaries | [`cli/sources.rs`](../contract/cli/src/sources.rs) | | CLI flags and output | [`cli/main.rs`](../contract/cli/src/main.rs), [`cli/build.rs`](../contract/cli/src/build.rs), [`cli/lib.rs`](../contract/cli/src/lib.rs) (diagnostic JSON) | The design is recorded in LLP 1006, LLP 1017.000 and LLP 1035.005.000. A proposal in a design document is not an implemented grammar production. ## The implementation loop In an app made by `exact new`, run its own `exact.mjs` from the app's directory. `bun exact.mjs update` regenerates that file, so the app's own verbs go in `app.json`'s `commands` (`"seed": ["bun", "seed.mjs"]` is `bun exact.mjs seed web`, run in the app's directory), which it reads and update leaves alone. A command that prepares files a build reads but the app's repository does not hold, such as a script that fetches photos into a folder Git ignores, goes in `app.json`'s `before_build` (`["bun", "scripts/fetch.mjs"]`, where `bun` is the Bun that runs `exact.mjs`, so it runs on every host): each verb that builds, runs, compiles or checks the app runs it first in the app's directory, and stops if it fails. Make it finish at once when the files are there. `exact.mjs`'s `contract` verb is exact2's compiler, with paths relative to where you run it: ```sh bun exact.mjs contract types app.contract -o app.contract.d.ts # what app.ts imports bun exact.mjs contract build app.contract --json bun exact.mjs contract symbols app.contract bun exact.mjs contract fmt --stdout app.contract bun exact.mjs contract vocab padding # is it accepted, and how bun exact.mjs web ``` Inside the exact2 checkout, for its own apps, the same commands are `cargo run -q -p contract -- apps//app.contract …` and `bun host/web/dev.mjs --app `. Run `bun install --frozen-lockfile` there once. Use `build --json` before and after the edit. It returns one JSON array with all independent diagnostics it can collect, capped at 20; success is `[]`. It is the whole output: stderr stays empty (but for a note while the compiler first builds), and `bun exact.mjs contract build` appends `app.ts`'s type errors (`typescript-TS2322`, …) to the same array. Keep the exit status: 0 success, 1 compilation/I/O failure, 2 invalid invocation. Read all diagnostics, including related locations, before making the next repair. Each has a `severity`: `"error"` refuses the build; `"warning"` (after the errors, also on a success) builds and runs but is not what you meant, such as a header node the iOS bar hides (`lower-header-unplaced`), a route's `header` a box below the route, which iOS leaves in the page (`lower-header-nested`), a heading header at the head of an app with no navigation route (`lower-header-routeless`: a lone `navigationKey` is only a root; add a navigation root holding a separately keyed screen whose first child is the header, or give the header `appearance="none"` to keep it drawn as written on iOS), a content heading that says the bar's title again (`lower-title-twice`), a tab bar of one tab (`lower-single-tab`), a `button` with no words and no `aria-label` (`lower-button-unlabelled`), a dialog's role on a box (`lower-dialog-role`), an `aria-level` with no `role="heading"` (`lower-level-without-heading`), a content-box `width="100%"` with padding, wider than its parent by the padding (`lower-width-padding`), a box whose fixed width ceiling cannot grow with its text at accessibility sizes (`lower-text-px-width`; use `rem` for the width and its bounds), a bare `line-height=28`, which is 28 times the font size, not 28 points (`lower-line-height-multiplier`), screens swapped by an `if` over a state, at the top of the view or inside a route (`lower-screen-by-state`), a `replace` to another route, pressed on the screen a launch at `/` opens before anything else changes the router, which the router refuses (`lower-replace-root`), a count before a plural noun, `${n} cards` (`lower-count-plural`), a row of the app's records that searches another table of them, `users.filter(u => u.id == p.userId)` in `for p in posts` (`lower-row-lookup`), a row that walks its own list, `tokens.slice(0, i)` in `for t, i in tokens` (`lower-row-own-list`), a symbol by an alias, not its role (`lower-symbol-alias`), a `performanceNow()` time sent to a source (`type-performance-now-persisted`), the launch's `time.epochAtZero` or `time.seed` as a resource's identifying argument, or a value equal to one at launch (`time.epochAtZero + tick * 60000` while `tick` starts at 0), which keeps its kept answer from ever showing at launch: pass it after `with` (`lower-resource-launch-identity`), a state read after its own action assigned it (`analyze-read-after-assign`), a literal colour that stays the same in dark mode (`lower-colour-one-scheme`, every site past the cap), a `list` holding a `section` with no `appearance`, which is neither a grouped list nor a list of cells (`lower-list-section`), and a name or key a host would refuse or ignore at run time: a swipe row's `swipeTrailing` naming a button outside its `scroll` (`lower-swipe-wiring`), a navigation root's `navigationKey` that never meets its routes' (`lower-navigation-key`), a `navigationSplit` off the root route (`lower-navigation-split`), an `image` source that is a bare word, neither a symbol nor a file (`lower-image-source`), and a literal `RoundButton` label of more than the 7 characters its circle holds (`lower-round-button-label`); each names its repair, and fixing it is part of the edit. Formatting is explicit. `fmt --stdout` previews, `fmt --check` checks, and plain `fmt` writes. Formatting changes spacing and breaks only: a result that would parse to a different program is refused (`fmt-tree-change`) and nothing is written. Avoid formatting unrelated files. `symbols` reports definitions and references, with component interfaces and inferred action effects. Search by exact name with `symbols file.contract --name name`. After compiling, build and drive the actual app. From an `exact new` app: ```sh bun exact.mjs test web # the app's *.test.contract; builds a stale web app first bun exact.mjs agent web tree "tap add" state logs "screenshot out.png" bun exact.mjs agent web --size 390x844 "screenshot phone.png" # a phone-sized viewport ``` The web carrier opens at 420×900; `--size x` (before the operations) opens another viewport, and a test's first step `size x` does the same for that test. `resize x`, an operation and a test step, resizes it mid-drive as a person dragging the window's edge would: the browser's viewport, a macOS window, the Linux presenter (the driver's op also takes `resize `; a test step takes only `x`). iOS refuses a mid-drive `resize`; a size given at launch (`--size`, a test's `size` line) is laid out at that viewport with no safe-area insets, scaled to fit the app's frame (the safe area, unless the root covers the whole screen). `close`, an operation and a test step, presses the window's close button as ⌘W or the red button would (the agent's window is never key, so a typed ⌘W reaches nothing): the window asks its `beforeunload` first, and the reply says `closed: true`, or `closed: false` for a window a handler kept (the web's "Leave site?" is answered "Stay"). macOS and the web; iOS and Linux refuse it. `--storage ` gives a drive a named scratch store, kept between drives on macOS, iOS and Linux until one passes `--reset-storage`, and on web Chrome when the next drive passes `--keep-storage` (a browser profile served on one port per name; a Firefox or WebKit drive starts fresh each time, so a name there lasts for the drive, including its `reload`). A native drive that names none uses the store `default`, kept the same way, as the app keeps its files from launch to launch. A drive that compares with a reference (`screenshot … compare`) starts empty on every host unless it passes `--keep-storage`, so a plain drive after one can show what an earlier plain drive stored. A web drive without one has no app storage: an `app:/` file read finds an empty store (code `ENOENT`, as a fresh install's first launch does), and a write or SQLite request is refused (`storage is unavailable in agent mode unless the drive names a scratch store (--storage )`), so an app that keeps its data there does nothing on a write; a chosen document (`doc:/`) still opens under its grant. That store, and an authored test's own store, includes a data module's `secret.keep`: files in the named scratch tree on Apple and Linux, and the page's `localStorage` on the web. Apple native modules use that store's data, cache and temporary roots too; a fresh drive clears them, and only picked temporary documents are cleared on each launch. A nameless drive keeps no secrets. To show what survives a restart on any host, use an authored test's `reload` step (below). The driver prints each reply in a short text form. A `tap` or `type` is one line (`tapped #15 "add" · delivery platform · epoch 2 · clock 0 ms`); a failed one starts with `ERROR`. `state` prints one line a section (`slots {"count":1}`) and leaves out the empty ones (null, `[]`, `{}`); one over 8 KB prints its size, its small sections, the others' sizes, and how to read one (`state resources`, `state feed.0`) or all of it (`state --full`). `state ` reads a state, derive, resource or section of the record, even where a view carries the same name. Every other reply is one line too (`clock 600 ms · settled · epoch 5`, `screenshot out.png · 420×900 · …`). `--json` prints every field of every reply, one JSON object a line. `--scale ` shrinks each screenshot to n pixels a point: an iPhone's are 1206×2622 at its own 3, 402×874 with `--scale 1`, one pixel a point as on the web, at about a ninth of the pixels. An app's `bun exact.mjs agent ios|macos` takes `--scale 1` unless told otherwise; ask for more only to check fine pixels. On a simulator a `window` screenshot's pixels are the display's (`pixels="display"`): glass, blur and the keyboard as the device draws them. So are a plain one's where glass or a blur lies in the page; where the display cannot be read (the app not in front, an overlay) the reply says its materials are approximate, and why. Inside the exact2 checkout, for Caltrain: ```sh bun host/web/build.mjs caltrain-web bun scripts/agent.mjs web tree "tap change-station" "type station-search Palo" state logs bun scripts/agent.mjs web --test apps/caltrain/app.test.contract ``` For an external app driven from the exact2 root, set `EXACT_APP_DIR` on both build and drive. A stale artifact is a failed verification; rebuild what the driver names. A successful Cargo rlib build does not prove a native app launches or behaves correctly. tvOS uses the iOS presenter and plan: `bun host/apple/build.mjs --tvos caltrain-apple --run` builds and launches on an Apple TV simulator. TypeScript apps provision lean Hermes for `tvos-simulator` once per machine. On a container, `focusGuide="auto"` makes an entering remote move return to its last focused descendant, or its first focusable descendant. Other hosts ignore it, including when the value is bound. The agent driver has no tvOS target yet. A script drives the same session in JavaScript: `const s = await open({ host: 'web', app, epoch, timeZone, storage, size: [390, 844] })` from `scripts/agent.mjs` (`size` is `[width, height]`, or the CLI's `'390x844'`), then `s.tap(target, opts)`, `s.type(target, text | { key })`, `s.clock(arg)`, `s.tree()`, `s.state()`, `s.logs()`, `s.layout()`, `s.screenshot(path)`, `s.resize(width, height)`, `s.prefer({ … })`, `s.perf(target)` and `s.close()` — the CLI's operations by the same names. Their replies are the CLI's `--json` output: `s.tree()` is `{ roots, nodes: [{ id, type, depth, props: { testId, text, value, … }, children }] }`, every node in one preorder list (`depth` and `children` give the nesting), and `s.state()` is `{ slots, derives, resources, pending, … }`. `app` is the app's name; an app outside the exact2 checkout is found through `EXACT_APP_DIR` (its directory), as its own `exact.mjs agent` sets it, so a script run with plain `bun` sets it to the app's directory too (`EXACT_APP_DIR=/path/to/app bun verify.mjs`; a script that `app.ts` never imports is no build input); the working directory alone does not select the app, and `webDist` alone does not select an app. `s.op(request)` is the host's wire beneath them: it addresses views by numeric `id`, and it refuses a request it would answer by doing nothing (a `target`, an unknown op, a web `tap` with no browser input behind it). A reload or a raw browser step goes through `s.carrier` (`reset({keep: true})` reloads the current browser history entry with its store and agent launch facts, keeping Back and Forward entries; `evaluate`, and on Chrome `call`, drive raw browser steps; `evaluate` takes an expression string or a function of no arguments, which runs in the page, so it sees none of the script's variables). Use the existing six repository checks for repository changes. Do not add a new global check or fixture framework for an ordinary app edit. For documentation, compile complete examples, parse authored tests, and check local links. ## Language inventory | Form | Placement | Meaning | | --- | --- | --- | | `use A, B as C from "./file.contract"` | File | Names from another file; nothing unnamed comes along (LLP 1091); `contract fmt --uses app.contract` writes the lines `contract-use-missing` names | | `use Card from "@acme/ui"` / `use Activity from "exact:motion"` | File | A package's names (from `node_modules`), or a built-in's: `exact:motion`, `exact:content` ([content components](#content-components-exactcontent)) | | `shape Name` | File | Finite record with typed fields | | `fn name(arg: T): U = expr` | File | Effect-free, nonrecursive expression function | | `style Name` | File | Literal style attributes | | `keyframes Name` | File | Constant animation frames | | `font "Family" = "path.ttf"` | File | Bundled family; block form supports faces | | `routes nav` | Root file | Route table and implicit router state | | `component Name` | File | Root (first component) or reusable child | | `test "description"` | Test file, normally | Agent interaction steps; not a plan assertion block | | `props` / `inject` | Component | Typed interface / inherited bindings | | `provide` | Component | Whole-view provider bindings, one per line | | `slot` | Component | Accept caller-authored children | | `state name = expr` / `state name: T = expr` | Component | Stored state; its type is inferred unless declared | | `derive name = expr` | Component | Dependency-driven computed value | | `resource name = source(args) as shape T` | Root component | Reactive data request | | `mutation name as shape T [queue \| parallel]` | Root component | Optional reply slot for explicit sends | | `action name(args)` | Component | Event transaction; effects inferred | | `task name mount` / `task name when cond [key=expr]` | Root component | One timer/frame schedule or keyed change action, always or while `cond` holds | | `view` | Component | UI tree | Top-level declarations begin in column 1. Indent with spaces. Comments use `//`. Use double quotes for strings and backticks for templates. Names admit CSS hyphens, so write `a - b` for subtraction between identifiers. Units are quoted (`"12px"`, `"50%"`), not number suffixes. Element attribute continuation lines are deeper than the element and begin with `name=`. Parentheses permit multiline calls and expressions without creating an indentation block. Only sixteen words are refused as names, and only where a name is bound (a state, prop, parameter, `let`, `for` item, shape name, …): `when`, `if`, `else`, `for`, `in`, `match`, `case`, `as`, `fn`, `and`, `or`, `not`, `true`, `false`, `none`, `some`. Every other keyword (`key`, `state`, `from`, `refresh`, `view`, …) is an ordinary name there, so `action searchKey(key: string)` and `action refresh` compile. Shape fields, named arguments and members take any word ([grammar](contract-grammar.md#lexical-rules)). `contract vocab` prints the card, what an app writes in one read (at most 12 KB: every function, the tags with their attributes and events, each part's call, the test steps, the agent's operations, and how to do a second screen, a sheet, a timer, colour, data, a test; `docs/card.md` is the same text, checked in); `contract vocab --all` lists every built-in tag, attribute, CSS property and standard function the compiler accepts, with each property's value kind and default and each function's signature; `contract vocab ` answers for one (a function's rules and an example too: `contract vocab formatDate`), or says what to write instead (`contract vocab sort`). Check there before guessing at CSS or a function. ## Values, expressions, and functions An expression Contract accepts means what the same TypeScript means, except as this table says (LLP 1116.016 D1; `semantics/corpus/typescript/` checks each against JavaScript), and a TypeScript form Contract refuses is refused with what to write. Nothing runs because a value changed (no `useEffect` or watch). | Contract | Because | | --- | --- | | No truthiness: a condition is a `bool` (`if name != ""`) | every condition says what it tests | | Absence is an option (`none`, `some(x)`, `match`), not `undefined` or `null`; no `??` or `?.` | a value whose type has no absence is never absent | | A value never changes: no `push`, `sort`, `xs[i] = …`; `concat`, a record copy | an action is one transaction | | `==` compares lists and records by value; no `===` | values have no identity | | A string is well-formed: a lone surrogate becomes U+FFFD (`"😀".slice(0, 1)`) | every host holds Unicode scalar values | | A stored number is finite: a commit that would put `NaN` or `Infinity` in a state or derive is refused, and the write that caused it does not land | every host can carry it | | A resource or `send` argument must contain only finite numbers, including inside lists, records and options; the runner refuses the call before asking its source and names the argument position | Rust and TypeScript sources accept the same values on every host, whether placed on the main thread or a worker | | `toFixed` and the format functions print `""` for a non-finite number | one rule for every format | | Names hold hyphens, so subtraction takes spaces (`a - b`) | CSS's names | | A binding's list work is bounded (65,536 steps); a row's own binding has its own bound, so a row that searches another table costs rows × items (`lower-row-lookup`), and one that walks its own `for`'s list grows with it until the binding is unset (`lower-row-own-list`) | determinism on every host | | No comparator sort: `sortBy(xs, x => key)` orders by one number or string key, stably (`"desc"` third; a second key is an earlier `sortBy`). Grouping, joining, aggregates and a stored or locale order are the data module's; the view names an order it relies on where it reads it (`loadTasksByDueDate()`) | a view computes what it shows, the same on every host | - Types are `number`, `string`, `bool`, declared shapes, `option`, `T[]`, and `action` for behavior interfaces. Do not emit authored `any` or object types. - `[a, b, c]` is a list of its items, which may span lines and keep a trailing comma; the items have one type, met as a ternary's arms are (`[none, some(1)]` is an `option[]`; `[1, "a"]` is `type-list-item`). `xs.concat(ys)`, `xs.slice(start, end?)`, `xs.includes(x)` and `xs.indexOf(x)` are the web's array methods: `[...xs, x]` is `xs.concat([x])`, `includes` finds a string, number or bool (by SameValueZero, as the web does) and `indexOf` says where (by `===`: `NaN` is never found, `-1` for none). Over text, `s.indexOf(t)` is a position in UTF-16 code units, and `s.split(", ")` cuts text into a `string[]` (recipient chips). A list the screen keeps for the session — a selection, open or collapsed ids, per-row offsets — is `state` built this way; a list the app keeps across launches, or a server owns, belongs to the data module, and so do grouping, aggregates and a stored order (a view orders what it holds with `sortBy`): ```contract state collapsed = [] action toggle(id: string) collapsed = collapsed.includes(id) ? collapsed.filter(c => c != id) : collapsed.concat([id]) ``` - `none` and `[]` need an inferable element type. A state initialized by either usually gets that information from later assignments; a typed argument or the other conditional/match arm can also supply it, and a state may declare it: `state picked: option = none`, `state ids: string[] = []`. - An option directly inside an option is refused (`type-option-option`): `option>` written, `some(none)`, `first` or `at` of a `option[]`, a mutation `as shape option`. The web erases `some`, so `some(none)` would be `none` there; hold the inner option in a record field. A type nests at most 64 deep, through shapes, lists and options (`type-too-deep`). - One evaluation (an action body, a derive, a binding, a key, an argument) takes at most 65,536 list steps (each `map`/`filter` body run, each item `join` prints or `concat`/`slice`/`split` keep, each item `includes` or `indexOf` scans), builds strings of at most 64 MiB of UTF-8, and values of at most 2²⁴ values and 64 MiB of string bytes. Every target refuses the same step with the same reason: an action is refused with nothing changed; a prop or style binding's row is unset, with one journal line naming its site and the trap until it evaluates to a value again, and the app goes on; an `if`, `match` or `for` subject, a key, a row's state initializer, a surface argument or a virtualized list's own options stops the runner (LLP 1090, LLP 1017.003 D3). - A state's initializer runs before any resource answers and before any derive: it reads props, injects and the states declared above it, nothing else (`type-initializer-scope`). Derive a value from a resource instead, or keep per-row state in a component used inside `for`. - `Shape(field=value, …)` constructs every field exactly once. `Shape(base, field=value, …)` copies a base of the same shape and replaces fields. The one positional base comes first. Compiler-owned shapes are not constructible. - Use field access, arithmetic, comparisons, boolean operators, ternaries, and exhaustive option matches. There is no truthiness or optional chaining. - Inline match is `match x { case some(v) => a, case none => b }`; the comma is required and the `some` arm comes first. The block statement/view form has separate indented `case` arms. - `xs.map((x, i) => expr)` and `xs.filter(x => bool)` return values, not nodes. `xs.join(separator)` accepts primitive items. `first(xs)` and `xs.at(i)` return options; `at` supports negative indices. - Two strings compare with `<`, `<=`, `>`, `>=` in UTF-16 code-unit order, as on the web (`end > start` for `"HH:MM"` times). `s.slice(0, -1)`, `s.replaceAll(find, with)` and `s.toLowerCase()` are the web's string methods. - Numbers have `floor`, `ceil`, `round` (JavaScript's `Math.round`: `round(-2.5)` is -2), `min`, `max`, `%`, `formatNumber` (`"compact"` is `1.2K`, `"decimal"` `1,234.5`, `"percent"` `26%`, `"currency", "USD"` `$1,481.47`), `toFixed` and `formatDecimal`. A field's text is a number through `match parseNumber(s) { case some(n) => …, case none => … }`: a decimal numeral, trimmed, or `none` (`"12px"`, `""`). - Money is a count of cents printed with `formatDecimal(cents, 2)`, which is exact (`1234` is `"12.34"`, `-5` is `"-0.05"`). A price held as dollars becomes cents with `round(price * 100)`, which is right for a price of at most two decimals under a trillion (`19.99` is `1999`, though `19.99 * 100` is `1998.9999999999998`); past about `3.5e13` the double cannot hold the cents, and a half cent such as `1.005` has no exact binary value and becomes `100`, so keep money in cents from the source when amounts can have more places (tax, a split bill). `formatDecimal` of a number that is not an integer prints `""`, so round first. `x.toFixed(2)` is the web's `x.toFixed(2)`, for a measured number (`km.toFixed(1)`): it rounds the binary value too (`(1.005).toFixed(2)` is `"1.00"`) and sums of dollars drift (`0.1 + 0.2`), which is why money is counted in cents. Both take the digits as a whole-number literal (`toFixed` 0–100, `formatDecimal` 0–20), never a variable, and print `""` for `NaN` or `Infinity` (JavaScript's `toFixed` prints `"NaN"`). ```contract component Cart state cents = 1999 state km = 12.34 view column text `Total $${formatDecimal(cents, 2)}` testId="total" text `${km.toFixed(1)} km` testId="distance" ``` - Dates: `formatDate(ms, offset, "iso")` is `YYYY-MM-DD`, and `calendarDiff(from, to, "years")` (or `"months"`) is the whole periods between two such dates as an `option`, counted as an age is (a Feb 29 birthday has its year on Mar 1). `formatDay(then, now, offset, "day")` names a day from `now` (`Today`, `Yesterday`, else the date; `"day-time"` adds the time, `"list"` is a list row's, as Mail's). - There is no general list append: add an item to resource-backed data in its source and answer the updated list (a mutation that `refreshes` the list's resource, or its own answer). - The web's string and array methods are written as methods, with their names and meanings: `s.trim()`, `s.includes(q)`, `xs.filter(x => …)`, `n.toFixed(2)`, `xs.length`. The free spelling (`trim(s)`) is refused with the edit (`syntax-method-form`), and so is a method Contract lacks (`xs.reduce(…)`, `syntax-method-call`). The rest of the roster is functions: `first(xs)`, `round(n)`, `parseNumber(s)`, `formatNumber(…)`. There are no object literals, spreads, general lambdas, array indexing, assignment expressions, or JavaScript built-ins by implication. - `fn label(done: bool): string = done ? "Done" : "Open"` is a function: parameters and the return type are explicit, after `:` (not `->`). Its body is one expression over its parameters and standard calls (including `performanceNow()`), without component-state capture or recursion. Pass an app value in; do not invent an ambient reference. A `fn` named like a standard function (`fn indexOf`) shadows it in every free call of the app, so a standard function added later never breaks an app that had the name first; an action or a host command keeps its own rule. A method call (`xs.indexOf(x)`) is always the built-in: calling one whose name the file also declares is refused (`syntax-method-shadowed`). - Named arguments belong to component uses, record constructors, `t("key", placeholder=value)`, `empty(field=value)`, canvas `surface=` bindings, the commands `share(…)`, `scrollIntoView(…)`, `playSound(…)` and `stopSounds(…)`, and a native module's sources and sends (`showNotification(…)`). Every other function takes positional arguments. For the full roster and special calls, see [standard functions and intrinsics](contract-grammar.md#standard-functions-and-intrinsics). For record behavior in a callback, use [records](../contract/corpus/records.contract) and [the record tests](../contract/cli/tests/it/records.rs). ## State and action semantics Action reads see the starting state. Writes commit together. They do not provide imperative read-after-write visibility. Bind intermediate calculations with `let`: ```contract component App state count = 0 state doubled = 0 action advance let next = count + 1 count = next doubled = next * 2 view button press=advance testId="advance" text `${count}/${doubled}` testId="result" ``` The first press yields `1/2`. Writing `doubled = count * 2` instead would use the old count and yield `1/0`. The same snapshot rule applies inside nested branches and to locals declared after a state assignment. The compiler warns of such a read (`analyze-read-after-assign`), naming the line that assigned the slot, and of a send and an assignment of one newest-wins mutation on one path (`analyze-send-assigned`): the assignment forgets the request in flight and keeps its value, whichever comes first, so a retry hides the old result while `pending(m)` instead, and a logout sends through a mutation of its own. A local is immutable, block-scoped, and visible only after its declaration. It cannot shadow an in-scope name. Two disjoint arms may declare the same local name. An action can assign only its own writable state/mutation slots, never a prop, derive, resource value, or arbitrary record field. Replace a record with a copied record. `send` targets a mutation owned by that component. Permitted statements: assignment, `let`, `send`, `refresh`, known host command, a call of an action, `if`/`else` (`else if c` is `else` around one nested `if`), and option `match`. No loops. The compiler infers effects from the body, through its calls; `writes` is a refusal, not an optional annotation. Use `symbols` when you need the inferred write set. An action calls an action of its own component, an `action` prop or an injected action by name with its parentheses (`play()`, `arrive(path)`; a bare `play` is not a statement), anywhere a statement goes (LLP 1089). The call is the callee's statements run where it stands, in the same commit: one rollback, and the callee reads the state the action started with, as every statement does. A name that is a host command stays the command. Never give an action, an `action` prop or an inject a host command's name (`close`, `reload`, `setScheme`, `focus`, …) if you will call it: the call is refused, `syntax-call-ambiguous`, so a host command added later can't silently take it over. Binding such a name is allowed. A call gives no value; compute values with `fn`. Write a repeated sequence once and call it: ```contract component App state location = "/" state query = "" state sel = 0 action arrive(path: string) location = path query = "" sel = 0 action openItem(path: string) arrive(path) action goHome arrive("/") view column button press=openItem("/docs") testId="open" text location testId="where" button press=goHome testId="home" text "home" ``` Because the callee sees the starting state, a slot its caller assigned first is stale behind the call, and the compiler warns of that read as it does in the action's own body (`analyze-read-after-assign`, naming both frames): `sel = next` then `follow()`, where `follow` reads `sel`. Pass the value it should see instead: `follow(next)` for the new one, or a `let` bound before the assignment for the old one. Two calls that each read and write one slot (`move(1, 0)` then `move(0, 1)`) are named the same way: both read the starting cell, and the last write wins. Calls in exclusive branches (`if k == "ArrowUp" …` then `if k == "ArrowDown" …`) are separate paths. A derive is not mutable storage, an async effect, or a timer. Derive cycles are refused. Avoid unnecessary state that can be calculated from existing values. ### A remembered setting: `persist` A root `state` that ends in `persist` is kept across launches (LLP 1116 D5), as SwiftUI's `@AppStorage` or a React `useState` over `localStorage`: ```contract component App state tipPercent = 18 persist state units = "C" persist action toggleUnits units = units == "C" ? "F" : "C" view button press=toggleUnits testId="units" text `${tipPercent}% · °${units}` ``` - Before the first frame the store is read: a kept value of the state's type replaces the initializer's. A missing one keeps the initializer's, and so does one of another shape (the state's type changed since), which `logs` names (`persisted state units: the stored value does not fit string: …`). - After a commit changes the state, a value within the limit is saved to the page's `localStorage` (`exact.state.`), the app's `UserDefaults` domain on Apple (never the Keychain), or a file under the app's data directory on Linux. The key is the state's name, so renaming the state forgets what was kept. - Small settings only: a number, string or bool, or an option or list of one, at most 16 KB as JSON. A longer value leaves the last stored value intact, and `logs` reports it once until it changes. `persist` on a record, a list of records or a state holding an action is `type-persist-type`: keep app data in a source. A child component's state is `type-persist-child`; a derive, `syntax-persist-derive`. - Each authored test starts from an empty store, and its `reload` reads back what it kept. A drive with `--storage ` keeps persisted state in that store between drives (on the web, the next one passes `--keep-storage`; on a native host `--reset-storage` empties it); a drive without one starts from the declared values and keeps nothing past the drive. The recipe: [a remembered setting](recipes/persisted-setting.md). ### Editing a value: the field's contract An `input` handler should update raw draft state on every edit so live search, validation and button enablement follow typing. A text field fires `change` on Enter or blur; its handler should normalize the draft. Use `submit` for explicit submission. A text field is re-set only when what its `value` binding reads changes, never after each keystroke (React writes the bound value back; Exact does not, so a half-typed `-` or `1.` survives). That makes the contract: - **while editing**, the field is bound to raw text in state that `input` always writes; - **validation** reads the parsed value (`parseNumber`, a trim, a length) and shows a hint, without touching the text; - **on commit** (`change`, which a text field fires on Enter and on blur), the action writes the accepted value and writes the normalized text back into the draft, which changes the binding and redraws the field. Since `change` also fires when an edited field loses focus, it is no "submit on Return": a form submitting on it submits on a tap away from the field (an eye, a "Forgot password?"). Submit on Return with `submit`, which runs on Return alone, after the commit (and never for an IME's Return). A field bound straight to the accepted value breaks this: an action that normalizes `-2` to the `0` it already held leaves the binding unchanged, so the field keeps showing `-2`. So does normalizing while the person types: a `task … when draft != …`, a timer or an `input` action that rewrites the draft (an empty field back to `"1"`) puts text back under the caret mid-edit, and the next keystroke lands after it (`1` then `3` reads `13`). Normalize only in `change`. ```contract component Quantity state count = 1 state draft = "1" action edit(text: string) draft = text action commit(text: string) match parseNumber(text) case some(n) count = max(0, round(n)) draft = `${max(0, round(n))}` case none draft = `${count}` view column gap=8 input value=draft input=edit change=commit inputmode="numeric" aria-label="Quantity" testId="qty" text (match parseNumber(draft) { case some(n) => (n < 0 ? "Must be 0 or more" : ""), case none => "Enter a number" }) testId="qty-hint" text `Ordered: ${count}` testId="qty-count" ``` `type "qty" "-2"` leaves `-2` in the field with the hint; `type "qty" key "Enter"` commits, and the field reads `0`. A checkbox bound to a resource's field follows the same rule from the other side: it shows the resource's value, so it snaps back until the save answers and the resource is read again (LLP 1102 §3.16). ## Composition and lifetime The first component is the root. Each component use is `Name(prop=value, …)`. Supply every declared prop exactly once; props have no defaults (pass `none` for an option). An `action` prop or inject takes an action name or `action(args)` with captured arguments; the eventual event payload is appended at invocation. To choose between actions, pass one action that makes the choice in its body. A conditional supplied as an action prop or inject, including through a derive, is refused at the argument or provider binding. A child derive that aliases an action name or captured call can supply either binding. Value props and injects replace value reads only: beside `min: number`, `min(4, 9)` still calls the function, in views and actions alike. An `action` prop or inject named like an app `fn` is refused when an expression in the component calls that name and inlining could substitute the action into the call. Rename either declaration. Handler-only uses are allowed. An expression's `match` or arrow binder protects its bound name; a view's `for` or `match` binder does not protect call heads. A derive supplies a value, even when its expression is a function call. Beside `fn pick`, `derive pick = pick(1)` makes `${pick}` read that result; `pick(5)` still calls the function. States and device-fact resources also supply values only: beside `fn pick`, `state pick = 7` makes `${pick}` read `7`, while `pick(5)` calls the function. These names leave calls intact in derives, state initializers, actions and slot fills. A child's action may call its `action` props and injected actions, with the arguments after those captured where they were bound: the parent's action runs in the same commit, and the child never writes the parent's state itself. A swipe that decides, in the child, to tell its parent resets its own state, then calls the prop last in an `if` arm: ```text component Row props id: string archive: action state dx = 0 action release(dy: number) dx = 0 if dy > 120 archive(id) ``` Children may hold state, derives, and actions. They cannot declare resources, mutations, or tasks. Lift shared data requests to the root and pass values and actions. A child used under `for` owns row state keyed to that row's identity. Stable keys matter when content is inserted, removed, filtered, or reordered. A root that has outgrown the 1,500-line cap keeps its members and moves them into files by feature: `members "./composer.contract"` in its body, and in that file a `members App` block of states, derives, resources, mutations, actions, tasks and sequences. The program is the one-file program, and every file sees every member; each file `use`s the shapes and `fn`s it calls (`docs/contract-grammar.md`, "Components and actions"). A state cannot start from a resource, but a child's state can start from a prop. So a form that edits a saved record is a child made once the record is in: the source answers `loaded: true`, the placeholder says `false`, and the child's states take the record's fields when it is made. The initializer runs once per child: a later `draft` (a refresh, a normalized answer) does not reset the form, and while a refresh is out `saved` keeps its value, so the editor stays. A native host (and the wasm web target; the default JS target keeps none) can make that child from a kept answer. When the data module is not ready at boot, a resource whose source read device state (a file, a database, a secret, a watched topic) shows the last answer the runner kept for it (asked with the same identifying arguments, and at most 256 KiB counting them, 512 KiB for all of them together; `with` values are not counted), so the form's states take those fields. A larger answer is not kept, and an older one kept for the resource is forgotten, so a launch never shows an answer since replaced; `logs` says `too big to keep` once, naming the resource and what to do. An identifying argument that changes every launch (the launch's time or seed) never matches the kept one: pass it after `with`, as `resource lib = library() with time.epochAtZero`; the compiler and `logs` both say so. An answer that must never reach the device's storage (a one-time code, a message shown once) ends its line with `transient`: `resource code = otp(request) as shape Code transient`. It is never kept, a launch starts from its placeholder, and an answer an earlier build kept is forgotten at the next launch. To keep no answers at all, an app's grants decide (a Health grant keeps none). Keep a first screen's resources small and move their bulk into a resource the first frame does not need. A write the resource does not hear about (a file or database save from a mutation without `refreshes saved`, not a `store` write it reads) leaves that answer behind: after a restart the form opens with the old fields and keeps them when the fresh answer lands. Refresh the resource after each write, as below, or key the child by a string or number from the answer (`for d in [saved] key=…`) so a different answer makes it again. ```contract shape Draft name: string loaded: bool component App resource saved = loadDraft() as shape Draft else empty(name="", loaded=false) mutation stored as shape Draft refreshes saved action keep(name: string) send stored = saveDraft(name) view main if saved.loaded Editor(draft=saved, keep=keep) component Editor props draft: Draft keep: action state name = draft.name action edit(value: string) name = value action save keep(name) view column input value=name input=edit aria-label="Name" testId="name" button "Save" press=save testId="save" ``` A component `provide` section lists `name = expr` or bare `name` entries. A descendant declares `inject` with typed fields. The nearest provider in the component-use chain wins and covers the providing component's whole view. The old view wrapper `provide name = expr` is refused. Use a small wrapper component if a subsection needs different values. `slot` declares a receiving component's one content slot. `children` places the caller's nodes in its view. The fill keeps its caller's variables and providers, not the receiving component's provider override. Do not invent named slots or render-function props. Imports resolve local `.contract` declarations, with cycle and app-boundary checks. Paths begin with `./` and cannot contain `..` segments. They do not load app logic or expand the available built-in function roster. Compiler-owned router names exist only with a `routes` declaration. ### Tabular numbers Use `font-variant-numeric="tabular-nums"` for **numeric columns** and **values that change in place**: prices in a ledger, timetable times, timers, scores, counters, progress percentages and recalculated totals. Use default spacing for prose, ordinary labels, static standalone statistics, and fields merely accepting numbers. Choose by how the value is used, regardless of its size or currency symbol: static columns need alignment, and updating headlines need stable digits. ```contract component App state balance = "$1,234.50" view text balance font-variant-numeric="tabular-nums" ``` Apply it to numeric text, keeping the platform font. It makes digits equal-width without monospacing letters. Font support is required; unsupported fonts remain unchanged. The property inherits. `normal` restores the font's default, which may already be tabular; Exact does not accept `proportional-nums`. Equal-width digits do not fix the whole value's width: `99` → `100`, signs, precision, separators and units can still move content. Reserve space for the formatted range when layout must stay still. Align numeric columns to a common edge (usually right for left-to-right numbers), with consistent units and precision where appropriate. Digits alone do not align decimals; do not pad with spaces or misleading zeroes. Supported for ordinary text on macOS, iOS, tvOS, Linux, Windows, Android and both web targets, including prerendered pages. Exceptions: - **Terminal:** ordinary digits already occupy equal-width cells. Explicit `font-variant-numeric` settings are currently rejected. - **Android editable fields:** unsupported on native `input` and `textarea`; they keep their font's default digits. - **Native buttons:** unsupported. Keep their typography; use separate `text` for a readout. This property also does not style Canvas/GPU drawing or native hatches. - **Text wrapping around shapes on web:** a paragraph using `tabular-nums` skips Exact's wrapping along image or shape outlines and uses ordinary browser layout. Keep readouts separate from prose that needs this wrapping. `exact:content` components set their own typography: `SummaryCard` uses tabular digits for `value` and `detail`. Use `text` for a static statistic needing default spacing. Check the actual font, locale and host, including fallback digits, `11` → `88`, `99` → `100` and large accessibility text. Widths can differ across hosts. Keep locale-aware formatting and accessible labels. See [a numeric readout](recipes/numeric-readout.md) for sizing. ### Content components: `exact:content` The platform answers the controls and the chrome; the app's own content is the author's to design (LLP 1116.000 D3): a summary at the top, bars where numbers compare, rows with a hierarchy, an empty state that says what to do. `exact:content` is a built-in library of nine components that draw it with nothing but roles (`-exact-secondary-background`, `-exact-secondary-label`, `AccentColor`, the system hues, the text styles, symbols, the determinate `progress`), so dark mode, Dynamic Type and the tint come with them: ```contract use SummaryCard, StatBar, RingProgress, DoneMark, EmptyState, RowText from "exact:content" shape Day key: string name: string ml: number fn ml(n: number): string = `${formatNumber(n, "decimal")} ml` component Water state total = 750 state days = [Day(key="mon", name="Mon", ml=1800), Day(key="tue", name="Tue", ml=2250)] action drink total = total + 250 view scroll height="100%" accent-color="-exact-system-teal" column gap=16 padding=16 RingProgress(label="of 2,000 ml", value=ml(total), fraction=total / 2000, tint=none) SummaryCard(label="Today", value=ml(total), detail="of 2,000 ml", progress=some(total / 2000), symbol=some("symbol:activity")) for d in days key=d.key StatBar(label=d.name, value=ml(d.ml), fraction=d.ml / 2000, tint=none) row justify-content="space-between" for d in days key=d.key DoneMark(caption=d.name, done=d.ml >= 2000) if total == 0 EmptyState(symbol="symbol:calendar", title="No water yet", message="Each glass you log adds to today.") button "Log a glass" press=drink buttonStyle="bordered-prominent" for d in days key=d.key RowText(title=d.name, detail="", value=some(ml(d.ml))) ``` | Component | Props | Draws | | --- | --- | --- | | `SummaryCard` | `label: string`, `value: string`, `detail: string`, `progress: option`, `symbol: option` | a card: the symbol and label, the value large in tabular figures, the detail beside it where the card is wide and under it where it is narrow, the fraction as `progress` | | `StatBar` | `label: string`, `value: string`, `fraction: number`, `tint: option` | the label and value (the value under the label where both do not fit) over a bar `fraction` (held to 0–1) of the width: the accent short of the goal, the system green at 1 or past it, `tint` (a colour role) when given; one `meter` to a reader | | `RingProgress` | `label: string`, `value: string`, `fraction: number`, `tint: option` | a ring filled `fraction` of the way round from the top, in the bar's colours over a grey track, the value large in its centre and the label under it, at the default body's sizes at every text size; 200 across, down to 120 in a narrower box; one `progressbar` to a reader | | `DoneMark` | `caption: string`, `done: bool` | a filled check in the system green, or an empty circle in grey, with `caption` (short: a weekday; `""` for none) under it; one image to a reader, named by its caption and state ("Mon, done") | | `EmptyState` | `symbol: string`, `title: string`, `message: string`; a slot | a centred symbol (none at the accessibility text sizes), the title as a heading, the message; the nodes indented under the use (your `button`) under them; a `scroll` where it is taller than its space (in a flex column, give the column `min-height=0`) | | `RowText` | `title: string`, `detail: string`, `value: option` | a title, a secondary line under it (`""` for none) and a trailing value in tabular figures (under them where they do not fit on one line): a grouped list `row`'s content, or any list's | | `Badge` | `label: string`, `tint: option` | a status in a word ("Full", "Cancelled") in a capsule, the caption style semibold, keeping its own size beside a title; `none` the neutral grey, `some("AccentColor")`, or a system hue under white ink (dark on the light hues: green, mint, teal, cyan, yellow, orange); Windows contrast themes use the system highlight/text pair for these tints; read as its text | | `RoundButton` | `label: string`, `tint: option`, `press: action`, `disabled: bool`, `testId: string` | the platform's own button as an 80-point circle (the Clock's Start, Stop, Lap): filled with `tint` (a role: `"AccentColor"`, `"-exact-system-red"`), ink the host picks for contrast; a light hue (green, mint, teal, cyan, yellow, orange) is the platform's tinted style, the hue on the hue at low opacity; `none` is the neutral grey with the label's ink; the title stays the default body size at every text size | | `AnswerTile` | `label: string`, `status: string`, `press: action`, `testId: string` | a full-width tile for one answer, the label in the headline style; `status` `"idle"` (a neutral fill), `"selected"` (the accent's outline and checkmark), `"correct"` (the system green, dark ink, a check), `"wrong"` (the system red, white ink, a cross), `"dimmed"` (the rest, once told), or `"chosen"` (the person's own pick once told or closed: the selected look, disabled, so a closed poll still marks the vote while the rest dim); told, dimmed and chosen tiles are disabled; as a grouped list's row it is the list's cell on every host (`-exact-row-look`), the checkmark its accessory; a reader hears "Quito, correct" | - Props have no defaults: pass `none` for an option. A symbol is an `image` source: a role (`"symbol:calendar"`) draws on every host, `"symbol:sf/"` on Apple only. - They are in the app's accent: `accent-color` on the root (or any ancestor) tints them as it tints the controls, on the web and iOS (macOS keeps the system accent, as AppKit does). A state is a second colour: progress short of its goal is the accent, a goal met (`fraction` 1 or more, a card's `progress` at 1) the system green, done against not done green against grey. A `tint` overrides it: a budget gone over passes `some("-exact-system-red")`, a breakdown whose bars are shares rather than goals passes `some("AccentColor")`. - They draw no chrome, and two controls: `RoundButton` and `AnswerTile`, the emphasis a timer's or a quiz's screen needs, which paint on a `button` would make your own box. Any other action is your own `button`, beside a card or indented under `EmptyState`. Inset-grouped lists are for settings and forms; a content screen is a `scroll` of these. - Each fills its container's width; arrange them with CSS. In a `flex-wrap="wrap"` row, `flex="1 1 320px"` puts a card beside the bars on a wide window and stacks them on a phone. - The source is `contract/lib/content.contract`. To change one, copy it into the app. ## Data requests and side effects Choose the mechanism from its lifetime: | Requirement | Mechanism | | --- | --- | | Value follows query arguments | `resource result = source(args) as shape T` | | Source needs changing context while the same kept answer is suitable at boot | `resource result = source(identity) with context as shape T` | | Ask nothing until a condition holds (nothing selected, a query too short, a page not open) | `resource result = source(args) when cond as shape T`: idle while `cond` is false, asked again, forced, when it turns true | | Explicit command with a reply | `mutation reply as shape T`, then `send reply = source(args)` | | Re-request current resource arguments | `refresh result` | | Refresh reads around a mutation | `mutation … refreshes resourceA, resourceB` | | Show a write before its reply (optimistic) | `refreshes` on the mutation, and the source's `overlay` | | React once to a settled mutation | `mutation … then actionName` | | Writes that must all land, in order | `mutation … queue`: one in flight, later sends wait their turn | | Writes a person may repeat before the first answers: a rename, a save | `mutation … parallel`: every send asked at once, each reply lands | | Run once after a delay, while a condition holds (a toast, a debounce) | `task … when cond` with `after(ms, action)` | | Repeat while a condition holds (a game tick, a pulse) | `task … when cond` with `every(ms, action)` | | Act when a sheet or route opens (scroll the chosen item into view, load its draft) | In the root, even when the sheet is a child component's view: `task reveal when top(nav).name == "icons" key=top(nav).id` with `change(reveal)` | | Pending indicator | `pending(resourceOrMutationName)` | | "Loading" for a new question, not for a refresh | `not current(resourceName)`: false while it shows its placeholder, an answer to other call arguments, or a value kept across `when`'s idleness; a refresh of the same arguments leaves it true | | Resource request failed without an answer | `failed(resourceOrMutationName)` | | Why it failed, to branch on or show | `failure(resourceOrMutationName)`: `none`, or `some({ code, message, reason })`, `code` a domain refusal code when supplied, otherwise one of `offline`, `timeout`, `refused`, `shape`, `storage`, `error`; `reason` a Snapback refusal's code (`SLOT_FULL`), else `""` | | Initial resource fallback | `else empty(field=constant)`, or `else source(values)` answered once at build | A timer's step that reloads a resource runs the data module before the next frame, and the frame waits for it: a module that rebuilds every row each second drops a frame of a scrolling list each second, and `verify ios` says when the data module is in a late frame's time. Rebuild only what the tick changed (keep last tick's rows, remake those whose values moved), and leave out of the rows what the list does not show: a detail screen's chart points belong to a resource the detail reads. Resources read as their declared type. Mutations read as `option` and start at `none`, so a mutation's `T` is not itself an option. Do not treat a resource as an optional wrapper unless its declared type itself is optional. A mutation reply is unwrapped with match. An answer is held to its shape exactly: a field the shape does not declare, at any depth, fails the resource as a thrown error does (TypeScript lets a spread such as `{ ...row }` through). The view keeps its placeholder or last value and `failed(x)` is true, so a banner on `failed` reads "unreachable" for what is a shape mismatch: `state` names the field under `failed`, a CLI drive says so on stderr, and a failing `expect` names it. Project a backend row onto the shape field by field (`({ id: row.id, title: row.title })`). `failure(x)` says why, for the view to branch on: `none` while `x` has not failed, else `some` of a `Failure`, whose `code` is the nonempty domain `reason`, or the infrastructure class the same on every host (`docs/contract-grammar.md`), whose `message` is the text `state.failed` shows (for a developer; it differs by host), and whose `reason` is the string the source's error carried as `reason` (a Snapback refusal's code), else `""`: ```contract shape Item id: string component Items resource items = loadItems() as shape Item[] else empty() derive banner = match failure(items) { case some(f) => f.code == "offline" ? "You're offline" : f.code == "shape" ? "This app needs an update" : "Couldn't load items", case none => "" } view column if banner != "" text banner testId="banner" ``` `offline`, `timeout`, `refused` and `storage` reach the view only when the TypeScript module lets the `fetch` or storage rejection through (rethrows it, or does not catch it): an error it makes of its own, one for an HTTP 500 among them, is `error`. Its own error keeps its `message`, and `failure(x).message` is that text. An error with a string `reason` property keeps that too, as `failure(x).reason`: the snapback4 driver's `refused` throws a server's refusal with its code there, so a query's `require … else NOT_MANAGER` is `f.code == "NOT_MANAGER"` in the view. Mutations expose their latest admitted request failure too; a new send, assignment or success clears it. `bun scripts/agent.mjs web "fail fetch https://api…"` fails those fetches as a lost connection, so the banner says "You're offline". `with` takes one or more expressions, before `as shape`, and appends them to the source's arguments. All arguments still trigger re-asks and identify live requests. Only an eligible persisted answer admitted while the source is unready at boot matches the call arguments alone; it can then stand while activation is pending. Without `with`, every argument identifies that kept answer. Keep account/tenant IDs and representation choices in the call, credentials below the seam, and use `refresh`/`refreshes` for counters whose only purpose is another ask. The default web JS target keeps no persisted resource answers (LLP 1027.005). Resource inputs are not asynchronous prerequisites, and HTTP request lanes do not make resources depend on one another. A resource that reads another resource uses its current displayed value; it does not wait for the upstream request to succeed. While that request is pending, the value may be a retained answer, and `not pending(upstream)` does not prove the latest request succeeded. An authenticated source must await the app's shared current-session helper before issuing its protected request. It must verify that the session authorizes the principal named by the resource call. The helper and auth generation are app code, not Exact APIs. The app must advance its auth generation and clear its authorized principal whenever the principal changes or signs out. A request that must belong to that auth attempt should take the generation as a resource argument and return the principal and generation as fields in its answer. The view should show private data only when both fields match the currently authorized principal and generation. Checking only the owner cannot distinguish signing into A, then B, then A again. A pending session resource, or one that is merely not pending, does not prove authorization. Before committing a refreshed session or shared owner-sensitive cache write, recheck the generation at the commit point, including inside queued work. When possible, partition persistent caches by owner so one owner's valid result can still populate its cache after the view changes. If writes share a slot, guard the commit by owner and auth generation. Do not suppress a valid per-owner cache write only because another request became current. See the [fetch/loading/error recipe](recipes/fetch-loading-error.md) for retained-answer and owner-gating patterns. The [REST starter](patterns/rest/app.contract) uses public development identities and does not implement an authenticated session lifecycle. The current request owns its answer; older replies cannot overwrite a newer request. A newer send or an assignment drops a newest-wins mutation's in-flight reply. It does not undo storage operations the source has already started; those operations and any serialized storage chain behind them run to completion. Other source work, such as a fetch, may be aborted when an answer is replaced (see the [runtime reference](reference.md#what-a-data-module-can-use)). The action sending a newest-wins mutation (neither `queue` nor `parallel`) must not also assign its slot to `none`. Assignments commit after sends, so the reset can discard the new reply even when it appears first. Keep the mutation for its reply and hide an old error while `pending(added)`, as the [after-a-write recipe](recipes/after-a-write.md) does. `none` means the slot contains no reply, not that the write failed: use `failure(added)` for a request failure, and match a shaped reply's `ok` or error for a domain refusal. An action that sends one mutation twice on one path is refused (`analyze-send-twice`) because the second send would forget the first reply, counting the sends of the actions it calls. Every `if` is read as one that can run: two sends are separate paths only as arms of one `if … else if … else` or `match`, or in `if`s testing one unchanged name against different literals. A `match` binder and an outer name it shadows are different values; testing them against different literals does not make the paths separate. Send one combined request, use a mutation per request, or declare the mutation `queue` (LLP 1092) or `parallel` (LLP 1161.000). A `queue` mutation (`mutation wrote as shape Ack queue then afterWrote`) keeps one request in flight; every later send waits, in order, with the arguments it was made with, and is asked after the reply before it and that reply's `then`, so `then` runs once per reply, in send order. `pending(m)` is true while a send is in flight or waits: send while it is pending, since `not pending(m)` means the spinner is off, not that a send may be skipped. Assigning a queue's slot forgets nothing — every reply still lands over it — so a mutation that must drop a late reply (a session's sign-in) does not declare `queue`. At most 64 sends wait; the 65th refuses its action. A `parallel` mutation (`mutation renamed as shape Ack parallel refreshes issues`) asks every send when it is made, each its own request, and lands every reply in whatever order they come: two renames, two saves of one form, two comments. A reply lands in the slot unless a later send has already concluded, so a late reply never overwrites a newer outcome; `failure(m)` shows a send's failure unless a later send has already concluded, and an earlier send's success leaves a later one's failure standing. `pending(m)` is true while any send is out, an assignment forgets no request, at most 64 sends are in flight (the 65th refuses its action), and it takes no `then` (`type-parallel-then`), since its replies land in any order. Its source is called once per send, with several calls outstanding: one that reads local storage, changes it and writes it back must order its own work, or the mutation should be `queue`. Keep the default, newest-wins, for a reply that must be droppable, such as a sign-in. `refreshes` asks its resources again when the reply lands; a mutation the source answers synchronously (without a request) has landed, so its resources are asked in the sending commit. A send still in flight does not ask them; what it writes shows before its reply only through the data module's `overlay`, if it exports one (below). `then` is parameterless, runs once at the host's next clock advance as a new commit (under the driver, an input's own answer's `then` before the input's reply), reads the latest answer, does not run for a failure that brought no answer, and cannot send its own mutation; to repeat, use a task (see "Repeating while a condition holds"). Do not mistake the scheduling boundary for a general async workflow or a per-reply event log. A long poll (`/changes?since=…&wait=…`) is a stream too: add `exactLongPoll: { cursor: 'since', from: 'seq' }` beside `exactStream`, and the host asks again with each reply's cursor, skipping heartbeats and backing off on failures (`docs/reference.md`, LLP 1016.000 D8). Re-read on each event with `resource view = load() with live.seq`. A bidirectional WebSocket uses a resource's TypeScript `fetch(url, { exactStream(event, socket) { … } })` (LLP 1016.000 D7). Keep the handle in module state by resource identity; after its `open` event, a mutation source can call `socket.send(textOrBytes)` or `socket.close(code, reason)`. The mapper may also send and close on its own socket while it answers `open` or a `message` (a subscribe, an Ack per frame), with no `fetch`. A mapper may be `async` (inflating a frame with `DecompressionStream`, splitting a large parse with `await scheduler.yield()`): later frames wait for it, in order; after its first `await` it sends as an answer's turn does natively and cannot send on the web. Sends run after the commit that settles the mutation or the frame stands, in order, and have bounded admission. A mutation's source that sends answers now or with a promise, and cannot also `fetch` (whichever comes second refuses it). A resource's answer may send its own request on its module's socket and await the reply, with no timer, and so may a mutation whose source answers with a promise: what each turn of it sends goes out once the commit that ran that turn stands, and stays sent if it is let go (on the web build a later turn has no commit of its own: what it sends goes as the turn ends, once the commit that asked the answer stands). Refreshing or removing the resource invalidates the old handle. **TypeScript on the web JS target, macOS, iOS and Linux**; the wasm target and Rust sources remain receive-only. See [the reference](reference.md#generate-typescript-data-source-types) for bounds and events. A large response needs a parse that yields. On native hosts each turn of an answer has 100 ms of its own (CPU) time: from its call, or from the `fetch` or storage step it awaited, until it next awaits one. A turn over that is refused; a resource keeps its value and is asked again twice before it fails, so a parse that passes its tests on a 300 KB thread fails on a 1 MB one. `await scheduler.yield()` (the web's [Prioritized Task Scheduling](https://wicg.github.io/scheduling-apis/#dom-scheduler-yield)) ends the turn, and the answer goes on in a new one with its own 100 ms. Yield every few thousand items in a parse or tree walk: ```ts for (let i = 0; i < nodes.length; i++) { if (i % 2000 === 1999) await scheduler.yield(); comments.push(toComment(nodes[i])); } ``` The function that yields is `async`, and so is every caller that awaits it. On the web the page has no such budget, and a yield lets it paint mid-parse. ### Optimistic writes: the overlay To show a write before its reply, the mutation declares `refreshes` and the data module exports an `overlay`: ```contract shape Item text: string component List resource items = loadItems() as shape Item[] mutation added as shape Item refreshes items action add(text: string) send added = addItem(text) view column for item in items key=item.text text item.text ``` ```ts import type { Overlay } from './app.contract.d.ts'; type Item = { text: string }; export const overlay: Overlay = (source, args, answer, writes) => { if (source !== 'loadItems') return undefined; // other resources show their answers const shown = [...(answer as Item[])]; for (const w of writes) { if (w.source !== 'addItem') continue; const [text] = w.args as [string]; if (shown.some(i => i.text === text)) continue; // the answer has it if (w.answered) w.keep(); // the server has not caught up yet shown.push({ text }); } return shown as never; // `Result` cannot be narrowed from a generic `S` }; ``` From the send, derives and views read `items` as the overlay's value. Without an overlay they read the answer, and the write shows only once a refreshed answer has it. The runner calls `overlay` for each resource a write affects, with: - `source` and `args`: the resource's source name and arguments. Resources can share a source with different arguments; check `args` when a write belongs to one of them. - `answer`: the resource's current answer. Copy it rather than changing it. - `writes`: the sends that affect it, in send order. Each has `id`, `mutation` (the mutation's name), `source` and `args` (the send's), `reply` once its reply has landed, and `answered`: its reply has landed and `answer` was asked after that, so `answer` should include it. A write shows until the first answer asked after its reply landed. If that answer does not include it yet (a server that is not read-after-write consistent), call `keep()` on it to go on showing it. The runner does not ask again on its own, so a kept write shows until a later answer (from `refresh`, another write's reply, or new arguments) that the overlay does not keep it over. Returning `undefined`, throwing, or returning a value outside the resource's shape shows the answer and stops showing every answered write; the journal says why for the last two. A write that ends without landing stops showing in that commit, with no undo code, and the resource is asked again: its reply fails, its wake expires, or a newest-wins mutation's send is replaced by a newer send or an assignment to its slot. If that ask fails, `failed(items)` is true and `items` keeps its last answer. A `parallel` mutation's writes each show until their own reply and the answer asked after it, and one's failure takes nothing else with it; the overlay receives the writes that have landed, in the order they landed, then those still out, in send order. - The overlay runs synchronously inside the commit, so it cannot await, and it has no effects: `fetch`, storage, the store (`get` included), `native` and `crypto` (`crypto.getRandomValues()`, `crypto.randomUUID()` and every `crypto.subtle` call) refuse with an error naming the call, the same on every host: a call that returns a promise rejects, any other throws. The runner reuses its value while its inputs are unchanged, so compute it from them alone. - Writes are not saved: after a relaunch or a reload only answers show, and a resource a write was showing in is asked again. - A source whose module runs on a worker shows answers only, said once in the journal. - A Rust source implements `DataSource::overlay`, returning `Ok(Some(Overlaid { value, keep }))`, or `Ok(None)` to show the answer. - The agent's `state` shows what the overlay shows; `state.writes` lists the writes still showing, as `{ id, mutation, landed }`, when there are any. A failed resource retains its value or placeholder, with `pending=false` and `failed=true`. Argument changes, refresh, or successful answers clear the failure. A domain error returned as a record is an answer and must be handled as data. Infer the actual source interface from the Contract: ```sh bun exact.mjs contract types app.contract -o app.contract.d.ts bun exact.mjs contract rust app.contract -o /tmp/shapes.rs ``` Use those generated declarations with the existing TypeScript/Rust integration. A shape has no exported name in the `.d.ts`: name one by its source, `type Recipe = Result<'recipe'>` (a list's element: `Result<'recipes'>[number]`; an optional answer is `… | null`, so `NonNullable>`). `app:/data`, `app:/cache` and `app:/tmp` exist on every host before a source runs, so a file directly in one (`app:/data/notes.json`) needs no `mkdir`; a file deeper down needs its folder first (`storage.fs.mkdir('app:/data/drafts')`, which makes the folders above it too), or the write fails with `ENOENT`. The [human guide's data-module section](contract-for-humans.md#writing-the-data-module) has a complete `app.ts`: synchronous, `fetch` and SQLite sources, the grants each needs (one per line: `['sqlite.open app:/data/books.db', 'net.fetch https://…'].join('\n')`; `net.fetch` takes an `http` or `https` origin, `http://127.0.0.1:8080` too, or a whole scheme, `net.fetch https:` (any host and port, for a server the person names at run time; `net.websocket` takes `wss:` or `ws:` the same way); on iOS cleartext `http` reaches only a local host, and only with `app.json`'s `host.ios.localNetworking` set, unless ATS is off (below); a device on the local network the person names or the app finds is `net.fetch local-network` with `device.local-network purpose.`, whose text is the platform's prompt on macOS and iOS ([recipe](recipes/local-network.md)); the app's own `http` to any other host, such as a server the person types in, needs each Apple platform's `appTransportSecurity` (`host.macos`, `host.ios`) set to `{ "allowsArbitraryLoads": true }`, which turns ATS off for every connection the app makes, frames included, and which App Store review asks the app to justify; an `iframe` of `http://` from a named host, or such a sub-resource of the app's own `assets/` page, needs `host.macos.appTransportSecurity` or `host.ios.appTransportSecurity` set to `{ "allowsArbitraryLoadsInWebContent": true }`, which relaxes web views only (ATS lets an IP address or a `.local` name through without it, LLP 1020.001 §4 Q6, though on an iPhone those also ask for Local Network access), and how to drive it with storage. The macOS driver launches the assembled app bundle, so its ATS policy also applies during drives. A path grant covers its path and what is below it, by whole names: `sqlite.open app:/data` covers `app:/data/inbox-amy.sqlite`, `sqlite.open app:/data/inbox` does not. A refused open names the file it wanted and the grant line that would admit it. A line of `app.ts`'s grants that begins with the word `development` is a development build's only: a production bake (`EXACT_UPDATE_TRUST=production`, which an Apple `--archive` or `--release` and a deploy set) leaves it out, and any other keeps it without the word (`development device.calls purpose.calls`, Harmony's test call, LLP 1133.033 §13 question 4). Only the build's copy of `app.ts` changes, so the module's own `grants` constant is the build's, and the app can hide what the line serves (Harmony's `testCallBuild` source). A Rust data crate's `grants()` can't mark a line yet: the bake refuses one (`grant-development`). The bake's own trailing word on a development build's `local-network` line (LLP 1133.036 D5) is another thing. A token, a password or a key the module keeps is a secret, not a file: grant `secret.keep ` (one line per name, `secret.keep signal.token`) and use `store.set(name, value)`, `store.get(name)` (a string, or `null`) and `store.forget(name)` in an answer (LLP 1018). It is the Keychain on Apple; on the web, the page's `localStorage`, readable by any script on that origin; on Linux, unencrypted files in the app's data directory, retained across launches (an app without an ID keeps them in memory). A drive keeps it, by default, only in a named `--storage` store (`EXACT_STORE=real` gives an Apple drive, on a Mac, a simulator or a phone, the Keychain a normal launch reads). A secret that only the person in front of the device may read (an account's token behind Face ID) carries a policy on its line, `secret.keep token user-presence` (`biometry-any`, `biometry-current-set`, `device-credential`), then `optional` when the person chooses whether it is locked; the app grants `device.local-auth ` and depends on `@exact/local-auth` (LLP 1133.010). Its value is never in the store: `store.get` throws `InvalidAccessError` while it is locked, `await store.unlock(name, { reason })` shows the platform's sheet and answers the value, `store.has` and `store.locked` say what is kept, and `store.protect(name)` and `store.unprotect(name, { reason })` turn an `optional` lock on and off. `store.set` keeps every write in the name's state, a rotated token's too. Keep the value out of every answer: an answer is kept and shown. `authenticate(reason=)` from the same package is the check alone: it gates a screen, never a secret. The web keeps no locked secret yet: a name that must be locked is refused there, and `localAuthStatus().presenceSecrets` is false. Under the agent the sheet is a `presence` ticket (`tap @presence allow`, `cancel`, `fail`, `lockout`, `invalidated`), after Face ID's `permission` ticket while it is `prompt`. The compiler accepting a source call does not provide its implementation. Check its arguments, declared result, grants, storage access, and bake-time behavior. Keep generated output out of version control. Use app-local sources for domain formatting or algorithms beyond the finite standard roster. A bake runs initial data work and packages first-frame values. A first-frame value is not the answer: every host asks the TypeScript module again at launch, natively once it loads after first pixel, even a source with no arguments (`logs`: ` shows its build-time answer until its source answers`, then ` answered: …`). A resource with an argument (one after `with` too) made from `exactTime()`, through a derive, another resource or a state an action assigns it (or writes under a condition on the date), gets no first-frame value, since the bake's date is unknown: it shows its placeholder until the module answers with the launch's date, so a task waiting on `lib.loaded` never adopts an answer stamped at 1970. Don't pass the clock into a source whose answer doesn't depend on it, or its first frame waits for the module: format times in Contract (`formatTime(at, time.utcOffset, "short")`). A `send` an action makes before the module loads (a document opened at launch) waits for it, `pending` meanwhile (`logs`: `send : waits until the data source is ready`). Live requests run after that under host scheduling. Do not assume a secret store, disk database, or authenticated network session is available while baking. See [the data-module reference](reference.md#generate-typescript-data-source-types). ### Sign-in providers (`@exact/oauth`) The `oauth` recipe is the code; this is each provider's configuration, as its documentation requires (LLP 1146; the IDs are `packages/oauth/test/PROVIDERS.md`'s). | Provider | Configuration | |---|---| | Google | `issuer: 'https://accounts.google.com'`, `issuerAliases: ['accounts.google.com']` (GOOG-2), `authorizationParams: { access_type: 'offline', prompt: 'consent' }` for a refresh token (GOOG-8). Grant `net.fetch https://oauth2.googleapis.com`, its token endpoint's origin (GOOG-1). An iOS client's callback is its reversed client ID, `com.googleusercontent.apps.:/oauth2redirect` (GOOG-5); a desktop or web client needs its `clientSecret`, which is not secret (GOOG-4) | | Microsoft Entra ID | `issuer: 'https://login.microsoftonline.com/common/v2.0'`, `issuerTemplate: true` (ENTRA-1), `offline_access` in `scopes`. Register the callback as "Mobile and desktop"; send no `clientSecret` (ENTRA-3). A web build's callback is registered as "Single-page application" (ENTRA-4) | | Auth0 | `issuer: 'https://.auth0.com/'`, the tenant's domain with a trailing slash (AUTH0-1); `offline_access`, with "Allow Offline Access" on the API (AUTH0-3); `authorizationParams: { audience }` for an API's token. With rotation's overlap period on, `retryLostRefresh: true` | | Okta | `issuer`: the authorization server's issuer, `https://.okta.com/oauth2/default` (OKTA-1); `offline_access` (OKTA-4); `retryLostRefresh: true` while refresh tokens rotate with a grace period (OKTA-6) | | OneLogin | `issuer: 'https://.onelogin.com/oidc/2'` (ONELOGIN-1); no `offline_access`, which it refuses (ONELOGIN-2); `endedErrors: [{ error: 'invalid_request', status: 401 }]` (ONELOGIN-4) | | GitHub | `endpoints: { authorization: 'https://github.com/login/oauth/authorize', token: 'https://github.com/login/oauth/access_token' }`, no `openid` (GH-1), `clientSecret` (GH-7), `commaScopes: true` (GH-5), `endedErrors: ['bad_refresh_token']` (GH-4). Its token endpoint does not answer a browser, so a web build needs a server (GH-8) | | Salesforce | `issuer: 'https://login.salesforce.com'`, the `refresh_token` scope (SF-14); it sends no `expires_in`, so the expiry is learned from a 401 (SF-1) | | Amazon Cognito | the endpoints are on the user pool's domain, not the issuer's: grant both origins (COGNITO-1) | | ForgeRock (PingAM) | `issuer`: the realm's, `…/oauth2/realms/root/realms/`, as its discovery document names it. An app with its own sign-in screens answers the authentication tree (FR-1), then passes `signIn` an `authorize` that POSTs the authorization request with the SSO token (FR-2; below), natively only. A gateway that answers 403 for an expired access token: `expiredStatus: [401, 403]` (FR-4) | | Sign in with Apple | not supported: its token exchange needs a server-signed client secret | Every option, with what it does, is in `@exact/oauth`'s `index.d.ts`. **Tokens outside HTTP.** Use `provider.accessToken(store)` in data code when the intended API takes its access token somewhere `provider.fetch` cannot reach: a third-party SDK, or a message after a WebSocket opens, on a host whose mapper receives a socket handle. Prefer `provider.fetch` for HTTP requests. An OpenID Connect service such as Convex instead takes `provider.idToken(store)`, with that service configured to trust the issuer and audience. An ID token is not an API access token. Like `idToken`, `accessToken` synchronously answers what the store holds now, or `null` when signed out. It reads no clock and makes no refresh of its own. When the service reports expiry (a socket's authentication failure or an SDK's error), await `provider.refresh(store)`, check its `ok`, and ask for the token again. This uses the same refresh as `provider.fetch` after an API refusal; concurrent callers refreshing the same held token make one refresh. A failed refresh that keeps the session leaves the old token; one that ends it leaves `null`. Handle the failure before attempting authentication again. Both tokens are credentials. Keep them in data code; never return them in a Contract answer, put them in state, or log them. Answers are kept and shown by the agent. The return types warn about this in their documentation; they are ordinary strings, with no compiler warning or runtime filter that prevents a credential from entering an answer (LLP 1018 D5, LLP 1157 D7). With a token in hand, the caller owns where and how it is sent: - Send it only to the API it was issued for, over an encrypted connection. `provider.fetch` checks the destination against `apis`; the getter cannot check what the caller or SDK does with the returned string. - Never put it in a URL. [RFC 9700 §4.3.2](https://www.rfc-editor.org/rfc/rfc9700.html#section-4.3.2) prohibits passing access tokens by the URI query method described in [RFC 6750 §2.3](https://www.rfc-editor.org/rfc/rfc6750.html#section-2.3). - Ask for the token again when authenticating or reconnecting. Do not hold a copy across a refresh or sign-out. The caller handles the service's authentication failure and reconnect protocol. `provider.fetch` still refuses `ws:` and `wss:`. If the API requires a credential on the handshake URL, it refuses an unauthenticated upgrade before `open`; a first message cannot authenticate it. Use the ticket exchange: call `provider.fetch(store, url, { method: "POST" })` over HTTPS for a short-lived, single-use ticket, then open `fetch('wss://…?ticket=…', { exactStream })` synchronously in a fresh source call. Keep the ticket in data code and obtain a new one for each reconnect. See the [socket-ticket recipe](recipes/socket-ticket.md). The following excerpt is only for an API that accepts the upgrade without a credential, then accepts an `authenticate` message and reports expiry with `token_expired`. It needs a mapper with a usable socket handle: worker-placed TypeScript sources and the web's wasm target do not support this send path (see `docs/reference.md`, socket sending). The app has checked that `wss://api.example.com/live` is the intended API and granted `net.websocket` for it. Add these sources to the app's `Sources` object: ```ts live: (_args, store) => { let expired = false; return fetch('wss://api.example.com/live', { exactStream(event, socket) { if (event.type === 'open') { const token = provider.accessToken(store); if (token) socket.send(JSON.stringify({ type: 'authenticate', token })); } if (event.type === 'message' && event.data === 'token_expired') expired = true; return { expired }; }, }); }, renew: async (_args, store) => await provider.refresh(store), ``` When `live.expired` becomes true, the app sends the `renew` mutation. After its successful answer, it refreshes the `live` resource to reconnect; the next `open` asks for the token again and sends it as the first message. The token never enters the URL, subprotocols or a Contract answer. Adapt the message and refusal handling to the service; do not retry indefinitely. **An app's own sign-in screens.** A provider that lets an app sign the person in with its own screens (ForgeRock's authentication trees) still issues an authorization code. The app sends the authorization request as the provider documents and answers where it was redirected; the package checks it and does the rest. The source that answers the tree's last step keeps its SSO token in the module and answers only that the tree is done: an answer is kept and shown, and whoever holds the token can sign in as the person. ```ts let sso = ''; // the tree's SSO token, set by the source that answers its last step const sources: Sources = { signIn: (_a, store) => { const token = sso; sso = ''; return provider.signIn(store, { authorize: async ({ endpoint, params, fetch }) => { const reply = await fetch(endpoint, { method: 'POST', redirect: 'manual', headers: { 'Content-Type': 'application/x-www-form-urlencoded', Cookie: `iPlanetDirectoryPro=${token}` }, body: new URLSearchParams({ ...params, csrf: token, decision: 'allow' }).toString() }); return reply.headers.get('Location') ?? ''; }, }); }, }; ``` This sign-in runs natively. On the web, the browser answers `redirect: 'manual'` with no status or headers (Fetch's opaque redirect) and drops a `Cookie` an app sets, so a web build needs a server to send the authorization request, as GitHub's needs one for its token exchange (GH-8). Grant no `auth.session` or `auth.callback`, and set `callback` to the redirect URI the provider registered. Send with the `fetch` that `authorize` is given, the package's transport, and throw an `OAuthError` to say why sign-in failed, giving it the failing reply's status. `claims(store)` reads the ID token's claims, for an API that wants one of them in a header; the `log` option hears what the session did, with no token in it, for a diagnostics screen; the `fetch` option carries every request the package makes, for a demo whose server is in the app. **Testing against a fake provider.** Name `/packages/oauth/test/fixture.ts` as the app's server in `app.json` (`"commands": {"server": ["bun", "/packages/oauth/test/fixture.ts", "--port", "4400"]}`, `"server": {"database": false}`). Its profiles (`spec`, `google`, `entra`, `auth0`, `okta`, `onelogin`, `github`, `salesforce`, `forgerock`) behave as those providers do. The issuer is `http://127.0.0.1:4400/p/`, except Entra's `…/p/entra/common/v2.0`, Auth0's `…/p/auth0/` and OneLogin's `…/p/onelogin/oidc/2`. Google's token endpoint is on the next port, `4401`: grant it too. GitHub has no discovery: its `endpoints` are `…/p/github/authorize` and `…/p/github/token`. `GET /p//api/me` answers the person. ForgeRock's tree is `POST /p/forgerock/json/authenticate` (the person is `ada@example.com`, password `lovelace`), and its API answers 403 for an expired access token. `authorize` completes the held sign-in against the fixture; `authorize cancel` cancels it. `request POST "/control"` changes what the fixture does next: `{"action": "expire-access", "profile": "spec"}`, and `revoke`, `fail-token`, `hold-token` (then `{"action": "await", "held": 1}` returns once the app's token request is held, and `release-token` lets it go), `deny-authorize`; `fixture.ts`'s `control` lists them all. ```contract-test test "signs in, shows the person, and is still signed in after a relaunch" request POST "/control" {"action": "reset"} tap "sign-in" authorize clock data expect text "name" == "Ada Lovelace" reload clock data expect text "status" == "Signed in" ``` ### Native sign-in (`@exact/sign-in`) The [sign-in recipe](recipes/sign-in.md) gives the call and server flow. On iOS and macOS, an Apple-only request opens AuthenticationServices in the calling session's window. The answer has an empty `id`, a `handle`, `configURL`, `isAutoSelected: false`, Apple's `user` and authorized scopes under `apple`, and `token.authorization` containing `id_token`, `code` and `state`. First-authorization name and email, when supplied, are in `token.user`. A dismissal returns `NetworkError`, as the web does; runtime cancellation returns `AbortError`. Pass the raw server nonce in `params.nonce`. Both the native request and the web authorization URL carry it unchanged. Apple's [nonce property](https://developer.apple.com/documentation/authenticationservices/asauthorizationopenidrequest/nonce) and [ID-token claims](https://developer.apple.com/documentation/signinwithapplejs/authorizationi/id_token) describe checking the supplied value in the token. Exact does not hash it on either host. Construction and fixture tests cover this rule; comparing real Apple tokens from both hosts remains a device/provider qualification. The `auth.apple` grant derives `com.apple.developer.applesignin`; there is no separate app.json switch. Enable Sign in with Apple for the app's Apple Developer App ID and use a provisioning profile that grants it. The build signs the entitlement on a simulator or when the signing profile allows it. On macOS, a build without the signed entitlement reports Apple unavailable and requests return `NotSupportedError`. iOS leaves entitlement validation to AuthenticationServices; an authorization failure returns `IdentityCredentialError` with Apple's code. A simulator run does not establish successful sign-in on a signed device. Google on Apple, `getCredentialState`, and the combined Apple/passkey sheet remain unimplemented. The existing native capabilities still advertise Google active requests and the Apple/passkey combination. A combined `publicKey` and passive `identity` call still belongs to passkeys; this Apple-only path does not implement it (LLP 1133.014 D10). ### Convex live queries The `convex` recipe is the code for `@exact/convex`; this is what it leaves out (LLP 1157). - **The deployment's address.** `convex(address, …)` takes its `CONVEX_URL` (`https:`) or the same origin as a socket's (`wss:`), so one build-time setting can name both it and the `net.websocket` grant. - **`live.status`.** `connecting`, `open`, `ready` (results have arrived), `closed` (the socket ended or never opened: refresh `live`, as the recipe's `retry` does) or `failed` (Convex said something this client cannot follow; `live.error` says what, and a new connection would meet it again, so nothing retries it). A frame over the module's ceiling is one: the first frame of a connection carries every query's result, 8 MiB together unless `convex(url, functions, { maxMessageBytes })` says another. - **A quiet socket.** Convex sends a frame every 15 seconds. The module asks its socket with `exactIdleTimeout`, so the host ends one that carries nothing for a minute (`convex(url, functions, { idleTimeoutMs })` says another), in real time, whatever the driver's clock. It ends as `closed`, which the recipe's `reconnect` opens again. - **A query's lifetime.** Its identity is its function and arguments. One a screen no longer asks for is dropped at the second change after its last ask. One asked while the socket is closed, with nothing delivered for it, fails and is asked again when results arrive. - **Values.** A Contract shape is closed (every field, no others) and holds no `bigint` or bytes: map a result with `cx.query`'s fourth argument. A value outside its shape fails that resource; the socket goes on. - **Failures.** A failed query throws `ConvexFailure`: the resource fails and keeps what it showed. A mutation or an action rejects with one when Convex refuses it (`message` is Convex's, `data` a `ConvexError`'s payload), when the socket is not open (nothing was sent), and when the socket closes before the answer. That last one may have run, and nothing is sent again on the next connection: a write that must not repeat carries its own key. When Convex refuses a token it closes the socket. A request sent behind the refused `setAuth` rejects with Convex's words, as `live.authError` has them: it did not run. When a token expires, Convex refuses it at whichever request meets it first, and one sent earlier (an action still running) may have run, so each rejects with "Convex refused the token before it answered (…); the mutation may have run". - **An action's reply** arrives as soon as Convex has it, and the queries may show what it wrote a moment later: a mutation's reply waits for them, an action's does not. A view that reads both can show them out of step. - **A person's token.** `cx.setAuth(jwt)` in a mutation's source, with a JWT the deployment accepts (its `auth.config.ts`); `cx.setAuth(undefined)` signs out. In an `async` source it may follow an `await`; in a plain `.then` callback its send throws on the web. With `@exact/oauth`, signing in is one source: `async (_a, store) => { const r = await provider.signIn(store); cx.setAuth(provider.idToken(store) ?? undefined); return r; }`. When `live.authError` says the token expired, a source that awaits `provider.refresh(store)` and calls `setAuth` the same way renews it. The token stays in data code. - **One mutation, one socket send, no `fetch` in the same turn.** `setAuth` sends on the socket, and the runner refuses a mutation that both sends and fetches (`a mutation cannot both send on a socket and fetch`): nothing is sent and its `then` does not run. Fetch first in an `async` source, await it, then call `setAuth`: `async () => { await fetch(signOutUrl, …); cx.setAuth(undefined); return … }`. If a call's frame never left anyway, the module still gives no answer for the wrong person and sends each mutation behind the token again, so Convex runs none as the person before; calling `setAuth` again sends it again. - **After a token change** no answer computed for another token is given, across a reconnect too, until Convex delivers it for the new one. The screen keeps what it showed until the new answers land: gate a signed-in screen on your own session, as with any backend. A refused token is `live.authError`; Convex closes the socket, and the next connection is signed out. - **Renew a token before it expires.** Convex refuses an expired token only when a request or a query's rerun meets it, not on a quiet socket. Renew it from a Contract task (every 10 minutes for a token that lives an hour) with the same `refresh`-then-`setAuth` source. - **Tests** run with no Convex. `server.ts` beside `app.ts` gives `@exact/convex/test/server.ts` the app's functions as plain JavaScript over memory, and `test` and `verify` start it (`apps/mosaic/server.ts`); its `app.json` needs `"server": { "database": false }`, since the stand-in keeps no database. A function's second argument is who asks, `{ identity }`: the `sub` of the JWT the client sent, unchecked, or `null` signed out. Convex's HTTP API answers too (`POST /api/mutation`, `/api/query`, `/api/action`, `Authorization: Bearer `), so a test's `request` can write as another client; `http: (request) => Response | undefined` answers the app's other routes (an auth endpoint), with CORS headers added. Keep the rules both run in a file with no Convex import (`convex/tiles.ts`). One server serves a whole run: a test that counts clears first. - **Driving a native host against a real deployment.** The driver seeds a native host's randomness (`EXACT_AGENT_SEED`, 1 by default), so two launches draw the same session id and Convex answers the second's writes with the first's answers, without running them. Give each launch a new seed: `agent macos --seed 7 …`. - **Hosts.** The web, macOS, iOS, Linux and Windows. A worker-placed source and any other host have no socket to send on. There, and for a read that need not be live, use Convex's HTTP client under `net.fetch`: `new ConvexHttpClient(CONVEX, { logger: false })` from `convex/browser` (its default logger calls `Math.random()`, which a data module refuses), then `await http.query(api.messages.list, { channel })`. ### Child processes (`@exact/child-process`) A desktop app's data code starts a program with Node's names (LLP 1133.007). A resource answers `spawn(program, args, {exactStream})`: the child lives while the resource holds it, and its mapper hears `spawn`, `stdout`, `stderr`, `exit`, `close`, `drain`, `ready` and `ready-timeout`, with the incarnation's handle (`write`, `end`, `kill`). An answer awaits `execFile` for a one-shot run. Each program needs a grant line naming it (`process.spawn path:ssh`, `process.spawn /usr/bin/tar`); a call with none is `grant-missing`. Only a person's input or the app's own code starts or drives a child: a tool's call, a wake or a device event is refused `person-required`, and its commit lets no child go. Under the agent nothing runs unless the drive names it: `--process =script:` answers from a script on the agent's clock. macOS and Linux run it; every other target answers `NotSupportedError`, which `processStatus()` says ahead. The options, events and limits are `docs/reference.md`'s "Child processes". ## Views, layout, and interaction For an attachment of any type, use `input type="file"` with no `accept` (or `accept="*/*"`) and `multiple` when needed. `showPicker("attach")` opens the input with `id="attach"`; `change` receives `Picked[]` with copies under `app:/tmp/picked/`. A copy's file name is not the file's: `path` is `app:/tmp/picked/.`, `` from the launch and a counter, and `name` is the original name. Read the bytes at `path` (grant `fs.read app:/tmp/picked`), and show or upload them under `name`. A specific `accept` list remains bounded to image/video types and the app's `file_handlers`. For audio, declare `audio/mpeg`, `audio/wav`, `audio/mp4` or `audio/ogg` in `file_handlers` and name those exact types in `accept`; `audio/*` remains outside the admitted wildcard types. XHTML uses `application/xhtml+xml` in the same way. On Linux the agent can answer a held picker, but the live picker is unavailable (LLP 1069.002, "Any-type input"). The view forms are element, component use, `if`/`else` (and `else if`), keyed `for`, exhaustive option `match`, and `children`. Wrap root regions in a stable element. HTML names Contract spells otherwise (the compiler names each): `div` is `column`, `row` or `view`; `span`, `p`, `strong`, `em`, `b` and `i` are `text`; `h1`–`h6` are headings, as written; `label` is `text` beside its field, which `aria-label` (or `aria-labelledby`) names; `img` is `image`; `a` is `link`; `ul`, `ol` and `li` are a `list` or a `column` of rows; `title` and `meta` are `head title=… description=…`; a `table` is `view display="grid"` with `grid-template-columns` (or a `column` of `row`s); there is no `form` (a field's Enter is its `submit`) and no `details` (keep `open` in state, show the body `if open`). A view chooses with `if`, as an action does; a count is `xs.length`, never `count`, `len` or `.size`; a resource's placeholder is `else empty()` (`[]` for a list), never `else []`. A component call uses parentheses; a built-in element uses space-separated attributes. `button "Save" press=save` is text-child sugar; an explicit text child is useful when that label needs its own styling or driver id. A `button` is the web's `