# Declarative Navigation Every screen, transition, dialog, and discrete UI state in every MobileGym app is **declared** in a single file (`navigation.declaration.ts`) rather than encoded implicitly across React handlers. The declaration is the source of truth for: - The runtime `go()` / `back()` helpers each app uses. - Static analysis (consistency check between declaration and code). - Graph generation (BFS, shortest path, trajectory enumeration). - Task authoring (which buttons exist, what they do, what data they consume). This document is the formal reference. For the minimal walkthrough, see [`../../guides/add-an-app.md`](../../guides/add-an-app.md). > ๐Ÿงฑ **Why declarative.** Without static declaration, "what can the user do here" is buried across ` ``` The app's `go()` helper resolves the `tab` parameter against `cases` and picks the right destination. ### Conditions There are two condition shapes in current declarations: - `cases[].when` uses the runtime `Condition` shape, which compares route params/search values. - `ui.condition` and `uiStates[].stateCondition` use the data-mode `StateCondition` shape, which reads refs from the app config/state used by the analyzer. `cases[].when`: | Op | Example | Meaning | |---|---|---| | `exists` | `{ op: 'exists', ref: { ref: 'search', key: 'q' } }` | Ref is present and non-empty | | `eq` | `{ op: 'eq', left: { ref: 'param', key: 'tab' }, right: 'me' }` | Ref equals a primitive | | `in` | `{ op: 'in', left: { ref: 'param', key: 'type' }, right: ['a', 'b'] }` | Ref is one of the listed values | | `match` | `{ op: 'match', left: { ref: 'search', key: 'q' }, right: '^book' }` | Ref matches a regex | | `gt` / `gte` / `lt` / `lte` | `{ op: 'gt', left: { ref: 'param', key: 'count' }, right: 0 }` | Numeric comparison | | `and` / `or` / `not` | `{ op: 'and', items: [A, B] }` | Combinators | | `always` | `{ op: 'always' }` | Always true โ€” use as the final fallback `case` | ## Actions Actions are interactions that change the app's state but **not the URL**: toggling a like, ticking a checkbox, typing a search query, submitting a form. In current declarations, actions live on the `uiStates[]` entry where the control is visible. ```ts uiStates: [ { id: 'wechat.discover.feed', search: {}, description: 'Discover feed', actions: [ { id: 'wechat.discover.like.toggle', label: 'Toggle like on a post', scope: 'item', // omit for page-scope actions behavior: 'toggle', // 'toggle' | 'select' | 'input' | 'submit' | 'other' paramsSchema: { postId: 'string' }, // operand schema }, ], }, ] ``` ### Field reference | Field | Required? | Notes | |---|---|---| | `id` | yes | `..` for page-scope; `..item.` for `scope: 'item'`. | | `label` | yes | Human-readable label for graph viewers and task generation. | | `scope` | optional | `'item'` for per-list-item actions. Omit for page-scope actions. | | `behavior` | yes | Drives DOM-tagging style, graph treatment, and CI validation. `'toggle'` flips a bool, `'select'` picks one of several, `'input'` accepts free text, `'submit'` confirms, `'other'` is app-defined. | | `paramsSchema` | conditional | `Record`. **CI-enforced** for `behavior='input'` (must include `value`) and `scope='item'` (must include at least one object-identifier field of `'string'` or `'number'`). | | `effects` | optional | `ActionEffect[]` annotating local-state side effects: `{ kind: 'localState.open' \| 'localState.close', id: string }`. Used for documentation and Agent-training metadata; does not produce graph edges. | | `condition` | optional | Data-mode `StateCondition` filtering action visibility. | Action id grammar (regex `^[a-zA-Z0-9]+(\.[a-zA-Z0-9]+)+$`, minimum 3 segments, no `-` / `_`, app-internal unique) and the verb-suffix table (`.toggle` / `.select.