Everything else in this story is shaped by this answer, so it is settled first, in writing, with evidence.
What does the Postgres job engine build ON — Graphile Worker, pg-boss, or a hand-rolled queue?
| Graphile Worker | pg-boss | hand-rolled | |
|---|---|---|---|
| storage | Postgres only | Postgres only | Postgres only |
| wake mechanism | LISTEN/NOTIFY | polling + notify | ours |
| cron | built in | built in (schedule) | ours |
| debounce / singleton | no | yes (singleton / throttle) | ours |
| retries + backoff | built in | built in | ours |
| durable steps | no | no | no |
The last row is the point. No candidate implements memoized steps or a durable sleep — so the step shim is OURS to build under every option, and the choice is only about what sits UNDER it: a claim loop, a schedule tick, and a retry policy. That reframes the decision from "which framework" to "how much of a well-understood loop do we want to own."
A decision record under docs/decisions/ naming the choice and the evidence, structured as this corpus's other ADRs are. It must answer:
job_step, or whether we add our own beside them. A library whose schema we must extend is not obviously better than no library.FOR UPDATE SKIP LOCKED and with more than one worker, since that is what the worker subtask is built on.lib/jobs/ repeats that.docs/decisions/ record names the choice, the rejected options, and the reason each was rejected — with the reason for the winner stated as evidence, not preference.lib/jobs/defineJob.ts — the option surface any candidate must be able to express (retryPolicy, concurrency, idempotency, debounce, cron)lib/jobs/definitions/codeGraphRefresh.ts — the only debounce user, and the hardest option to reproducedocs/decisions/job-lane-occupancy.md — why this migration is happeningdocs/decisions/ — the ADR shape and header convention to follow