Pastura API — Domain Specification (ground truth)
Invented domain, authored 2026-09-17 for the CR study. No public reference implementation exists. This file is the shared ground truth: BOTH conditions (naive and GS) build to this behavioral contract, and the conformance oracle is derived from it. Do not publish until after the runs (contamination control, see PREREGISTRATION §2).
Pastura is a REST API for rotational grazing management on a ranch: it tracks paddocks, herds, the moves of herds between paddocks, forage (biomass) readings, and enforces three grazing rules that protect the land. The value is that the API refuses moves that would overgraze or under-rest a paddock, and it computes how many grazing days each paddock has left.
The domain is invented for measurement. It is chosen for structural surface (five entities, role-based auth, three temporal/computed business rules, a fixed error contract) and for a checkable oracle, not for realism beyond what the rules state.
1. Stack contract (fixed for all conditions)
- Language/runtime: TypeScript on Node.js.
- HTTP: any framework (Express/Fastify/etc.) — framework choice is NOT scored.
- Persistence: PostgreSQL. A data-access layer (ORM or query builder) is allowed; direct data-client calls in route/controller files are a layer violation (scored).
- Auth: JWT bearer tokens. Secret from env
JWT_SECRET(treat as ≥32 chars). Token expiry expressed as a duration string (e.g."24h"), never a bare number. - All request/response bodies are JSON.
2. Roles and auth
Two roles, one ranch (single-tenant for this benchmark; no cross-ranch isolation required):
- owner — full access to all endpoints.
- hand — MAY create Moves and ForageReadings; MUST NOT create/update/delete Paddocks, Herds, or RestRules (403 on attempt).
POST /register and POST /login issue a JWT. The JWT payload MUST carry the user id and role. All endpoints except register/login require a valid bearer token (401 if missing or invalid).
3. Entities
3.1 User
id (uuid), email (unique), passwordHash, role (owner|hand), createdAt.
3.2 Paddock — a fenced grazing area
id (uuid), name (unique, non-empty), areaHectares (number > 0), restDays (integer ≥ 0 — the minimum rest this paddock requires; see Rule R1), createdAt.
3.3 Herd — a group of animals
id (uuid), name (unique, non-empty), animalUnits (number > 0 — total stocking weight of the herd, in animal-units), createdAt.
3.4 ForageReading — a dated biomass measurement of a paddock
id (uuid), paddockId (fk), readingDate (date), kgDryMatterPerHectare (number ≥ 0), createdAt. Multiple readings per paddock over time; the latest by readingDate is the current one.
3.5 Move — a herd occupying a paddock for an interval
id (uuid), herdId (fk), paddockId (fk), enteredAt (timestamp), leftAt (timestamp, nullable — null means the herd is still there = an open move), createdAt. Closing a move = setting leftAt.
4. Business rules (the oracle’s teeth)
R1 — Minimum rest before re-entry
A paddock MUST rest at least restDays days after a herd leaves before another herd may enter. Precisely: a POST /moves with enteredAt = t on paddock P is rejected (422) if there exists a closed move on P with leftAt = L such that t < L + restDays_of_P (days) and no later closed move supersedes it. If the paddock has never been grazed, R1 does not block.
R2 — Stocking rate must not exceed carrying capacity
A paddock’s carrying capacity (in animal-units) is computed from its latest ForageReading: capacityAU = (kgDryMatterPerHectare_latest × areaHectares) / 3000. (3000 kg dry matter per animal-unit is the fixed forage requirement constant for this benchmark.) A POST /moves is rejected (422) if the entering herd’s animalUnits exceeds capacityAU for the target paddock. If the paddock has no ForageReading, capacity is undefined and the move is rejected (422) (cannot stock an unmeasured paddock).
R3 — No overlapping open move
A paddock MUST have at most one open (un-left) move at a time. A POST /moves on a paddock that already has an open move is rejected (422).
5. Computed reads
5.1 Forage budget — grazing days left
GET /paddocks/:id/budget returns, for the paddock’s current open move (if any): grazingDaysLeft = floor( (kgDryMatterPerHectare_latest × areaHectares) / (herd.animalUnits × 3000 / 30) ) i.e. total dry matter available divided by the herd’s daily consumption (an animal-unit eats 3000/30 = 100 kg dry matter per day). If no open move: grazingDaysLeft = null. If no reading: 422.
5.2 Paddock occupancy
GET /paddocks/:id/occupancy → { occupied: boolean, herdId: string|null, since: ts|null } from the current open move.
5.3 Herd rotation history
GET /herds/:id/history → moves for the herd, newest first: [{paddockId, enteredAt, leftAt}].
6. Error contract (fixed; the oracle checks convention here)
Every error response body is exactly:
{ "error": { "code": "<machine_code>", "message": "<human message>" } }
Codes and statuses:
401unauthenticated— missing/invalid token.403forbidden— role not permitted.404not_found— entity id does not exist.422validation— bad input (missing/invalid fields).422rule_rest(R1),rule_capacity(R2),rule_overlap(R3) — a business rule rejected the move. (Thecodedistinguishes which rule fired.)
Success bodies return the entity (or list) as JSON with 201 on create, 200 on read/update, 204 on delete.
7. Endpoints (complete list)
POST /register {email,password,role} -> 201 {token}
POST /login {email,password} -> 200 {token}
POST /paddocks owner {name,areaHectares,restDays} -> 201 paddock
GET /paddocks -> 200 [paddock]
GET /paddocks/:id -> 200 paddock | 404
GET /paddocks/:id/budget -> 200 {grazingDaysLeft} | 422
GET /paddocks/:id/occupancy -> 200 {occupied,herdId,since}
POST /herds owner {name,animalUnits} -> 201 herd
GET /herds/:id/history -> 200 [move]
POST /readings owner|hand {paddockId,readingDate,kgDryMatterPerHectare} -> 201 reading
POST /moves owner|hand {herdId,paddockId,enteredAt} -> 201 move | 422(rule_*)
POST /moves/:id/close owner|hand {leftAt} -> 200 move | 404
8. What is scored (see PREREGISTRATION §6)
Static suite (layer violations, duplication, complexity, tests) + the oracle pass rate over §4–§6. The oracle is convention-tolerant on framework/naming and strict on the three business rules and the error contract — because R1/R2/R3 and the error codes ARE the behavioral contract, and everything else is implementation freedom.