{
  "name": "Assignary",
  "tagline": "The assigner's system of record — schedule and coverage, legible to agents.",
  "description": "Assignary staffs officials onto events. The three console views that render schedule and coverage — the console, offers and roster — each emit their machine surfaces alongside the rendered page: inline schema.org JSON-LD, a copy-as-API affordance (curl / python / ts against the typed tRPC read surface), and a shareable deep link. They appear once an association is selected, which is when there is data to describe. Schedule and coverage state are readable straight from the DOM and reproducible from the API without operating the UI — there is no 'API mode' to switch on.",
  "principles": [
    "deep-links",
    "copy-as-api",
    "data-behind-viz"
  ],
  "deepLink": {
    "base": "/",
    "params": {
      "org": {
        "type": "uuid",
        "note": "the organization whose schedule and coverage to view"
      },
      "event": {
        "type": "uuid",
        "note": "an event on the console — shows its crew coverage"
      }
    },
    "examples": [
      "/?org=<organizationId>",
      "/?org=<organizationId>&event=<eventId>",
      "/offers?org=<organizationId>",
      "/roster?org=<organizationId>",
      "/roster?org=<organizationId>&event=<eventId>"
    ]
  },
  "whenToUse": {
    "answers": [
      "one organization's schedule, crew coverage and open seats, for a caller who is a member of it",
      "why a named official cannot take a named seat — the eligibility reasons and the drive, per open position",
      "the caller's own offers, accepted assignments and calendar file",
      "the caller's own side of a chapter: their roster row, their availability, their payouts, and whether they can be paid yet",
      "the roster of an organization: who is certified, and who has claimed their row",
      "how much of the standing-check ceiling a chapter's issued credentials have spent, by day, so a caller budgeting checks reads the number before it is refused rather than after"
    ],
    "doesNot": [
      "any organization the caller is not a member of: a read naming an `organizationId` is answered from their role in it, and `org.listMine` returns only the ones they belong to",
      "a credential an unattended caller can obtain for itself — see /auth.md: a Cognito token needs an interactive sign-in, and a capability token is issued deliberately by an assigner who is accountable for what it does, cut for one chapter, and unable to mint one wider than itself",
      "an assigner's settlement detail from this manifest's read list, which is curated to an official's own view of a chapter: `pay.mine` answers the caller about their own payouts and nothing about anyone else's. The chapter-wide figures are not missing from the API — `pay.quoteWeekend` and `pay.settled` are queries the OpenAPI document at /api/openapi.json describes, each requiring `org:manage` — they are absent from here, and the console view that renders them emits none of the three machine surfaces",
      "public or anonymous data of any kind: there is one procedure callable without a token, and it resolves an invite. A person's calendar feed at /api/calendar/<token>.ics is not a procedure and is not anonymous — the secret in its path is its credential, a person makes it from their own account screen and no issued credential can, and it answers only whoever holds that link",
      "a payment an agent can make: settling a chapter's weekend ends in a Stripe Checkout session somebody completes in a browser, so `pay.settleWeekend` and `pay.confirmCheckout` are absent from `act[]` and this surface advertises no payment protocol. What would add one is a chapter asking to settle unattended; until one asks, authorising the charge is a person's"
    ]
  },
  "api": {
    "base": "/api/trpc",
    "openapi": "/api/openapi.json",
    "transport": "tRPC v11 over HTTP. GET /api/trpc/<procedure>?input={\"json\":<input>} — the superjson transport wraps every input in a `json` envelope. A procedure whose input is {} takes no `input` parameter at all.",
    "auth": {
      "scheme": "bearer",
      "header": "authorization",
      "guide": "/auth.md",
      "metadata": "/api/.well-known/oauth-protected-resource",
      "note": "send `authorization: Bearer <token>` (set $ASSIGNARY_TOKEN to that full value), the token being a Cognito access token or an issued capability token. A credential is issued to you, never obtained by you: either a Cognito access token belonging to a person who signed in, or a capability token an assigner issues from the console (prefix `asgn_`, same header). A capability token carries its issuer's authority narrowed by its scopes, recomputed every request, so it cannot outlive what it was cut from. Which of the two an operation takes is its `security` in the OpenAPI document: one listing `cognitoBearer` alone refuses a capability token however it is scoped. A read taking `organizationId` requires membership of that organization, and a capability token calling it must have been issued for that organization. Some additionally require a role action, named as `requires` on the entry; a member without it is answered FORBIDDEN, and a capability token needs that action among its scopes, or `org:view` for a read naming none. Some answer about the caller rather than about the organization — `official.me`, `pay.mine` and `official.connectStatus` need a roster row in it, so a member who has never been added as an official is answered FORBIDDEN whatever their role. `availability.list` is the one read whose requirement depends on its argument: the caller's own is answered from membership, anyone else's needs `crew:assign`, so it carries no `requires`. `assignment.forEvent` and `official.list` answer every member and leave part of the answer empty without an action — each seat's outcome, expiry and dispute without `crew:assign`, and the assigner's notes without `officials:manage` — so neither carries a `requires` either; the OpenAPI document lists every such condition in `x-conditional-org-actions`. Two reads take no `organizationId` and are scoped by something other than membership. `org.listMine` is scoped to the caller and returns the organizations they belong to, which is where every other read's `organizationId` comes from; a capability token answers with the one organization it was issued for, since that is the only one it can reach. `credential.status` answers a verifier, who is by definition outside the chapter that recorded the credential, so there is no membership to check: what bounds it is an unguessable handle that reaches a reader only inside a signed receipt, and a per-caller daily ceiling."
    },
    "read": [
      {
        "procedure": "org.listMine",
        "input": {},
        "returns": "organizations the caller belongs to, with role"
      },
      {
        "procedure": "event.list",
        "input": {
          "organizationId": "uuid",
          "from": "YYYY-MM-DD",
          "to": "YYYY-MM-DD"
        },
        "returns": "one page of an org's events in a civil window, by start time, with seats taken of seats and the window's totals — games, confirmed, awaiting, and seats taken of seats on the games still to crew; `short: true` narrows the page to games still to crew — not called off, completed or started — with a seat nobody has accepted"
      },
      {
        "procedure": "event.get",
        "input": {
          "organizationId": "uuid",
          "eventId": "uuid"
        },
        "returns": "an event and its crew positions"
      },
      {
        "procedure": "assignment.forEvent",
        "input": {
          "organizationId": "uuid",
          "eventId": "uuid"
        },
        "returns": "the seats somebody holds on an event — offered and accepted — with who holds each; a declined, lapsed or withdrawn seat is the trail's to answer"
      },
      {
        "procedure": "assignment.mine",
        "input": {
          "organizationId": "uuid"
        },
        "returns": "the caller's offers and accepted assignments on games ahead or ended within the last 30 days, or whose outcome was recorded within the last 30 days or is disputed, by start time"
      },
      {
        "procedure": "official.me",
        "input": {
          "organizationId": "uuid"
        },
        "returns": "the caller's own roster row — certification, sports worked, home pin, travel radius and per-day cap"
      },
      {
        "procedure": "availability.list",
        "input": {
          "organizationId": "uuid",
          "officialId": "uuid"
        },
        "returns": "one official's availability and blackout windows; the caller's own needs nothing beyond membership, anyone else's requires `crew:assign`"
      },
      {
        "procedure": "pay.mine",
        "input": {
          "organizationId": "uuid"
        },
        "returns": "the caller's own payouts, newest first, with status and paid-at"
      },
      {
        "procedure": "official.connectStatus",
        "input": {
          "organizationId": "uuid"
        },
        "returns": "whether the caller can be paid yet — their Stripe onboarding state"
      },
      {
        "procedure": "admin.metrics",
        "input": {
          "organizationId": "uuid"
        },
        "returns": "coverage-at-a-glance counts, and what a new chapter has set up — sports it can field, venues, officials on the roster and how many have signed in, and offers ever sent",
        "requires": "events:manage"
      },
      {
        "procedure": "invite.preview",
        "input": {
          "organizationId": "uuid"
        },
        "returns": "the chapter join code and recent invites (assigner)",
        "requires": "officials:manage"
      },
      {
        "procedure": "venue.list",
        "input": {
          "organizationId": "uuid"
        },
        "returns": "the venues the chapter holds events at"
      },
      {
        "procedure": "sport.list",
        "input": {
          "organizationId": "uuid"
        },
        "returns": "what this chapter can field — the shared catalog plus the sports it defined itself, each with its level ladder and default crew"
      },
      {
        "procedure": "official.list",
        "input": {
          "organizationId": "uuid"
        },
        "returns": "the roster, with names and whether they have claimed"
      },
      {
        "procedure": "event.whyNot",
        "input": {
          "organizationId": "uuid",
          "eventId": "uuid"
        },
        "returns": "eligibility reasons and drive minutes per official per open seat",
        "requires": "crew:assign"
      },
      {
        "procedure": "assignment.ics",
        "input": {
          "organizationId": "uuid",
          "assignmentId": "uuid"
        },
        "returns": "an ICS file and a Google Calendar URL for an accepted assignment the caller owns"
      },
      {
        "procedure": "credential.status",
        "input": {
          "statusId": "uuid"
        },
        "returns": "whether one credential still stands — valid, expired, revoked or unknown — for a caller outside the chapter that recorded it, holding a handle that reaches them only in a signed receipt; no name, no chapter and no history, and every call counts against a daily ceiling"
      },
      {
        "procedure": "credential.usage",
        "input": {
          "organizationId": "uuid"
        },
        "returns": "the meter behind that ceiling: per day over the last week, how many standing checks each of the chapter's issued credentials made and how many the caller made, each against the daily ceiling it is out of. Never which credential was asked about — the counter does not record it"
      }
    ],
    "act": [
      {
        "procedure": "assignment.respond",
        "input": {
          "organizationId": "uuid",
          "assignmentId": "uuid",
          "decision": "accept | decline"
        },
        "scope": "offer:respond",
        "effect": "takes or refuses a seat the caller was offered; a take is refused if it would double-book them or if a credential the sport requires has lapsed since the offer"
      },
      {
        "procedure": "availability.set",
        "input": {
          "organizationId": "uuid",
          "officialId": "uuid",
          "kind": "available | blackout",
          "startsAt": "date",
          "endsAt": "date"
        },
        "scope": "availability:manage",
        "effect": "adds one window to the caller's own calendar"
      },
      {
        "procedure": "availability.remove",
        "input": {
          "organizationId": "uuid",
          "id": "uuid"
        },
        "scope": "availability:manage",
        "effect": "removes one window the caller owns"
      }
    ],
    "contract": "the fully typed surface is @assignary/api AppRouter; the deep-link grammar is the app's state machine"
  }
}
