- crows serve: HTTP API, Server-Sent Events per list, web approvals with a 10-minute fail-safe timeout; localhost only by default - Shared pipeline for command line and server; trust.changed events - EVENTS.md: server/UI contract; RIVE_CONTRACT.md: data-binding spec for the animator (one Crow artboard for v1) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UQnSw4RRWp8uqpj3nsnnXF
11 KiB
Rive contract: the crows.riv file
The animator builds crows.riv against this file, and the web code drives it from the events in EVENTS.md. Both sides build against this file, so change it first and then the code. Each animation exists only because an EVENTS.md event fires it.
Style: Duolingo-like. Chunky, expressive crows with big squash-and-stretch reactions and readable silhouettes at 96 pixels. The approve and reject buttons stay in HTML for v1 (see "Why buttons stay in HTML").
Rive approach: data binding, not inputs
Rive now marks state machine inputs as deprecated and recommends data binding for new work (migration guide, inputs). So this file uses view models in place of inputs:
- A view model defines the data shape. A view model instance holds the values, one per crow on screen (view models).
- Number, Boolean, Trigger, Enum, String and Color properties can drive state machine transition conditions or bind straight to scene elements (property types, transitions).
- An enum can drive a Solo when its values match the Solo children's names and order, and those names must be exported (enums).
- Layers play at the same time, one state per layer, and the lower layer wins when two layers animate the same property (layers). Any State reaches a state from wherever the layer is. Exit State stops a layer (states).
1. Artboards
| Artboard | v1? | Purpose |
|---|---|---|
Crow |
yes | One reusable character. It covers all 8 crows plus the Sentry and Elder, using a role enum that switches the costume. |
Perch |
later | A scene with a branch of crows rendered as an Artboard List of Crow items (lists), plus flock-wide celebrations. |
Splat |
later | A full-screen "gift on your windshield" overlay for the prank. |
In v1, the web page puts one Crow canvas per visible crow and lays them out with HTML and CSS. The Sentry and Elder are variants of Crow, not separate artboards, so the whole cast shares one rig.
Artboard spec: 256 × 256, transparent background, crow centered with its feet on a baseline 40 pixels from the bottom, so the crows line up in a row.
2. Crow artboard
State machine: CrowMachine. View model: CrowModel, set as the artboard's default view model.
CrowModel properties
| Property | Type | Values / range | Drives |
|---|---|---|---|
role |
Enum Role |
crow, sentry, elder |
Solo Costume (children named crow, sentry, elder, in that order). The Sentry wears a lookout cap and the Elder wears spectacles and a gray ruff. |
name |
String | e.g. Hugin, Sentry |
Text run nameplate under the feet |
pose |
Enum Pose |
perched, flying, waiting, grounded |
Body layer |
trust |
Number | integer 0–2 (0 stranger, 1 regular, 2 trusted) | Badge layer |
cheer |
Trigger | Reaction layer: happy hop with a wing pump |
|
sulk |
Trigger | Reaction layer: droop, single feather falls. Sad, never angry. |
|
levelUp |
Trigger | Reaction layer: the level-up celebration (spin, confetti, badge shine). It's the biggest moment in the file. |
|
levelDown |
Trigger | Reaction layer: badge dims, sheepish shrug |
|
prank |
Trigger | Reaction layer: mischievous grin, flies off frame, a "plop" sound cue, returns smug |
The 8 crows tell themselves apart by name in v1. A plumage Color property for per-crow tints is later polish.
Layers, states and transitions
Layer Body, a loop that's always running:
| State | Loop | Notes |
|---|---|---|
perched |
yes | Entry state. Idle breathing, blinking, a head tilt every few seconds. |
flying |
yes | Wings flapping, a slight bob. It reads as "working". |
waiting |
yes | Holds a rolled scroll up toward the viewer and taps a foot. It reads as "your turn". |
grounded |
yes | Sitting dazed with stars circling. It means failure or timeout. |
Transitions: Any State → each state when pose equals that value, with a duration of 200 milliseconds (ms). There's no Exit Time, so a pose change is instant to read.
Layer Reaction, one-shots on top of Body:
- Entry →
none, an empty state. - Any State →
cheer,sulk,levelUp,levelDownorprankon the trigger of the same name. - Each one-shot →
nonewith Exit Time at 100%. - Lengths:
cheerandsulkat most 0.8 seconds,levelUpat most 2 seconds,levelDownat most 1 second,prankat most 2.5 seconds. - Animate only what
Bodydoesn't need to own (face, wings, effects), or putReactionbelowBodyin the layer list so the reaction wins while it plays.
Layer Badge:
- Any State →
badge0,badge1orbadge2whentrustequals 0, 1 or 2, with a duration of 150 ms. badge0shows no badge,badge1a bronze chest feather,badge2a gold chest feather with a gentle shimmer loop.
3. Event → animation mapping
The web code keeps a map from item_id to crows[] (from item.answered) so that it can aim verdict at the right crows. The crow field values Sentry and Elder select the instances with role set to sentry and elder.
EVENTS.md event |
Condition | Instance(s) | Set |
|---|---|---|---|
list.started |
Sentry | Create and show the Sentry. pose = perched. |
|
item.triaged |
needs_approval true |
Sentry | pose = waiting |
item.triaged |
needs_approval false |
No animation. The HTML shows the mode and reason. | |
crow.flying |
crow (create on first sight) |
pose = flying |
|
crow.landed |
ok true |
crow |
pose = perched |
crow.landed |
ok false |
crow |
pose = grounded |
item.answered |
needs_approval true |
each of crows[] |
pose = waiting |
item.answered |
needs_approval false |
No animation. A verdict: auto follows. |
|
verdict |
target triage, approved |
Sentry | cheer, then pose = perched |
verdict |
target triage, rejected or timeout |
Sentry | sulk, then pose = perched |
verdict |
target triage, auto |
No animation. A trusted Sentry just keeps going. | |
verdict |
target output, approved or auto |
each of the item's crows[] |
cheer, then pose = perched |
verdict |
target output, rejected or timeout |
each of the item's crows[] |
sulk, then pose = perched |
trust.changed |
holder is a crow, to > from |
holder |
trust = to, then levelUp |
trust.changed |
holder is a crow, to < from |
holder |
trust = to, then levelDown |
trust.changed |
holder is flock |
No animation in v1 (HTML banner). Later: a Perch celebration. |
|
item.skipped |
No animation. The HTML strikes the item through, and any instance still flying for that item goes to pose = perched. |
||
requester.pranked |
one random crow on screen, or the Sentry if it's alone | prank. Later: the Splat overlay. The HTML shows message and a cooldown_s countdown. |
|
list.finished |
every crow on screen | pose = perched, then cheer |
Two web-side notes:
- Pranks.
POST /api/listsanswers a pranked requester with a 429 error and nolist_id, so there may be no event stream to deliverrequester.pranked. The web code treats that 429 response the same as the event and firesprankon the Sentry. - Badges.
trustis per crow and per task type, so a badge shows the level for the task type of the item the crow is working on now. The web code seedstrustfromGET /api/trustwhen it creates an instance, and updates it ontrust.changedonly whentask_typematches.
4. Naming conventions
The web code matches these strings exactly, so they're case-sensitive.
| Thing | Case | Names |
|---|---|---|
| File | lowercase | crows.riv |
| Artboards | PascalCase | Crow, Perch, Splat |
| State machines | PascalCase + Machine |
CrowMachine, PerchMachine |
| View models | PascalCase + Model |
CrowModel |
| Enum types | PascalCase | Role, Pose |
| Enum values | lowercase, one word, matching server strings where they exist | crow, sentry, elder, perched, flying, waiting, grounded |
| Properties (including triggers) | camelCase | role, name, pose, trust, cheer, sulk, levelUp, levelDown, prank |
| States and layers | camelCase states, PascalCase layers | perched, badge2, Body, Reaction, Badge |
| Exported objects | as listed | Solo Costume (children crow, sentry, elder), text run nameplate |
The web code binds one blank instance per crow, using API names from the web data binding docs:
const r = new rive.Rive({ src: "crows.riv", canvas, artboard: "Crow",
stateMachines: "CrowMachine", autoplay: true, onLoad: () => {
const vmi = r.viewModelByName("CrowModel").instance();
r.bindViewModelInstance(vmi);
vmi.enum("role").value = "crow";
vmi.string("name").value = "Hugin";
vmi.number("trust").value = 1;
vmi.enum("pose").value = "flying"; // crow.flying
vmi.trigger("cheer").trigger(); // verdict approved
} });
Keep autoBind off. Every crow needs its own instance, or setting one crow's pose would move them all.
5. v1 minimum vs. later polish
v1 minimum, the smallest set that makes the web UI work:
- The
Crowartboard, theCrowMachinestate machine and theCrowModelview model, named exactly as in section 4. - The
roleSolo with all three costumes. The Sentry and Elder must read as different from the crows at a glance. - The
nametext run. - The
Bodylayer with all fourposestates. - The
Reactionlayer withcheer,sulkandlevelUp. - The
Badgelayer with all threetruststates.
If levelDown or prank doesn't exist yet, the web code falls back to sulk and cheer, so they can ship a beat later without breaking anything.
Later polish, in rough order:
levelDownandprankreactions.- A
plumageColor property so each of the 8 crows has a distinct tint. - The
Splatartboard for the windshield gift. - The
Perchscene: an Artboard List of crows, murder huddles and split formations, and a flocklevelUpontrust.changedwithholderset toflock. - A streak ring, a Duolingo-style progress bar toward promotion at 10 clean approvals.
EVENTS.mdcarries no streak count today, so this needs astreakfield added toverdictortrust.changedfirst. - A Rive-drawn approve or reject button, if HTML buttons ever feel flat.
Why buttons stay in HTML
Approval is the safety gate, and the server treats anything other than an explicit yes as a no. Real <button> elements give keyboard, screen reader and focus support for free, and an animation glitch can never swallow a click. Rive decorates the moment by having the crow hold up its scroll. The click itself belongs to the browser.