State contract
Field-by-field reference for .greenroom/state-contract.json.
The state contract declares the screens and goals Greenroom will test, and maps source files to them. It gives a run coverage authority: a pass can be clear within its declared scope when its goals are verified; without a contract, clean runs cap at inconclusive. Visiting every screen does not prove that a user journey succeeded. The contract lives at .greenroom/state-contract.json and, like the manifest, is always read from the PR's base revision.
Top-level fields
| Field | Type | Required | Meaning |
|---|---|---|---|
schemaVersion | string | yes | Contract schema version, e.g. "1.0". |
routerKind | "hash-spa" | "expo-router" | "react-navigation" | "swiftui" | with graphPaths | Which deterministic extractor rebuilds your screen graph from source. |
graphPaths | string[], 1 to 20 | with routerKind | Where the extractor reads. See the pairing rules below. |
entryState | string | no | The state a fresh launch lands on. |
sharedSources | string[] | no | Repository-relative globs whose changes require all states to be checked. |
states | array, 1 to 20 | yes | The screens you declare. |
transitions | array, up to 400 | no | Edges between states. |
Router kinds and graphPaths
routerKind and graphPaths must be declared together; one without the other is a malformed contract and fails validation loudly, rather than silently falling back to model-based mapping.
hash-spa: exactly one path, the single source file defining your views object. Diff scoping is line-level.react-navigation: exactly one path, same single-file shape. Scoping for this kind is file-level, not line-level.expo-router: one or more directories containing the routed screens (typically yourapp/directory).swiftui: one or more directories, typically the target's source root.
States
Each state entry:
| Field | Type | Required | Meaning |
|---|---|---|---|
id | string | yes | Stable identifier, referenced by transitions and reports. |
route | string | no | The route or URL fragment that reaches it. |
screen | string | no | The component or view name (TodayView, CheckoutPage). |
variant | string | no | Distinguishes variants of one screen (empty state, error state). |
goal | string | no | Actions and observable outcome the driver must establish. Without one, the driver is asked only to reach and identify the screen. |
identity | object | no | Key facts that identify this state in an observation. |
sources | string[] | no | Repository-relative source globs used to attribute changes to this state. |
oracle | string | no | Contract metadata used by research fixtures. Production does not execute this string as an assertion; put observable expectations in goal. |
setupTransitions | string[] | no | Setup metadata used by research fixtures. Production does not replay these transitions automatically; describe necessary setup in goal. |
Transitions
Each transition names from and to state ids, optionally with the action (or actions, up to 10) that performs it and preconditions that must hold. Transitions let the mapper reason about what else a changed screen can affect downstream.
Transitions describe the graph; they are not an execution script. In production, the driver chooses actions from current observations to pursue each selected state's goal.
Test a completed journey
The workflow's goal describes the overall journey. When a state contract exists, the driver receives each selected state's goal. A full pass's verification judge checks the overall journey against the evidence; a scoped pass checks only its selected goals. The overall workflow goal is not automatically added as another driver task.
For example, “Reach Progress” can succeed by opening a tab with old history. To test saving a session, make the actions and new result explicit:
{
"schemaVersion": "1.0",
"entryState": "session-saved",
"sharedSources": [".github/**", ".greenroom/**"],
"states": [{
"id": "session-saved",
"screen": "Saved session journey",
"sources": ["src/**"],
"goal": "Inspect Progress and record the existing session count. Return to Today, start a session, complete every step, choose a 4-star rating, and press Finish session. Verify Progress now has one additional session and its newest row matches the exercise and selected rating. Navigate away and back and confirm that row remains. Existing history alone does not satisfy this goal."
}]
}Replace the source mapping and app labels with your own. This contract selects one journey checkpoint, which may visit several screens; it does not claim full app coverage. Use a fresh isolated test environment. If persistence across a reload or relaunch matters, ask for that explicitly and ensure your test app supports it. Merge contract changes into the base branch before opening the test PR.
System alerts in goals
If the app requests a permission at launch (notifications, tracking, location), the iOS system alert is the first screen the virtual user sees on every fresh install, not your entry state. The driver never grants a permission (tapping Allow is a policy denial), may decline (Don't Allow, Not Now), and continues on what the app shows next. Write that into the entry state's goal ("decline the notifications alert, then confirm Today lists exactly one session"), make sure the app proceeds after a decline, and keep screens that need the permission granted out of the contract; the simulator cannot pre-grant notifications and Greenroom does not edit its privacy database.
Start small
Start with a few observable goals and source mappings, including the primary user outcome. Add variants where correctness matters most, and inspect the first report's evidence before expanding. The schema caps states at 20 per contract deliberately: it describes your app's core surfaces and outcomes, not every possible screen.