[idea:rookery-cfps]alpha-package2026-08-20

@rookery/cfps

Models a venue and the calls it puts out. Each call is a single idea carrying its deadline, its portal, and what happened when it was answered.

The venue is the thing that recurs and the call is the round, so a programme you apply to three years running is one #venue and three #cfps pointing at it by label.

#import "@rookery/core:0.1.0": rookery
#import "@rookery/cfps:0.1.0": cfps
#show: rookery

#let TODAY = datetime(year: 2026, month: 6, day: 1)

/* Every call names one of these kinds, so a typo fails at the call rather
   than minting a round no ladder governs. */
#let (venue, cfp, cfp-state, panel) = cfps(kinds: (
  postdoc: (sort: "job", ladder: (transit: ("submitted",), terminal: ("offered", "rejected"))),
))

#venue("acme", title: [Acme University])[A programme that runs every year.]

/* State comes from the dates rather than a field. A future deadline with
   nothing sent is open. */
#cfp(
  "acme-postdoc-26",
  venue: <acme>,
  kind: "postdoc",
  deadline: datetime(year: 2026, month: 1, day: 1),
  today: TODAY,
)[A round that lapsed with nothing sent.]

/* A submitted stage and no terminal rung is in flight, and a terminal rung
   settles the round. Stage names are checked against the kind's ladder. */
#cfp(
  "acme-postdoc-25",
  venue: <acme>,
  kind: "postdoc",
  deadline: datetime(year: 2025, month: 1, day: 1),
  timeline: (
    submitted: datetime(year: 2024, month: 12, day: 1),
    offered: datetime(year: 2025, month: 2, day: 1),
  ),
  today: TODAY,
)[A round that was answered, and settled.]

/* Panels filter on exactly those words. Pass today: everywhere, as it has no
   fallback. */
#panel(state: "settled", today: TODAY)