Design notes on the points the spec leaves open:

- **Input:** the payload is "now integer and jobs", so I read `payload.now` and `payload.jobs`.
- **Dedup:** jobs are keyed by `id`. The last value wins, and it keeps its own original index. Output order follows the original index of the surviving jobs. A `Map` that deletes and re-sets a key moves the entry to the end, so I collect the survivors and sort them by index afterward.
- **Cancelled:** a cancelled job goes to cancelled if `cancel_wins` is true or it is not retryable. Otherwise it falls through to the normal checks.
- **Dead:** a job is dead if it is stale, `attempt >= max_attempts`, or its `last_code` is nonzero and not retryable.
- **Due:** a retryable job with a failure (`last_code` nonzero and in `retryable_codes`) gets `due = max(due, failed_at + min(base_delay * factor, cap))`. The factor is `2**max(0, attempt-1)` when exponential, otherwise 1. Jobs without a failure keep their due. A missing `due` or `failed_at` defaults to 0.
- **Ready vs. waiting:** `due > now` goes to waiting, with `at = due`. Otherwise the job is ready.
- **Ready sort:** priority descending, then created ascending, then index.
- **Dead sort:** by id (numeric if both are numbers, otherwise string comparison), with index as the tiebreak.
- **No mutation:** the input objects are never modified.

```json
{"files":{"main.ts":"import {run} from './core.ts';\nimport {POLICY} from './policy.ts';\nexport function evaluate(payload: any): any { return run(payload, POLICY); }\n","policy.ts":"export const POLICY: any = {\n  base_delay: 100,\n  cancel_wins: true,\n  dead_order: 'id',\n  dedup_key: 'id',\n  delay_cap: 150,\n  exponential: true,\n  fifo_ties: true,\n  max_attempts: 3,\n  priority_high_first: true,\n  retryable_codes: [429, 500],\n  stale_after: 1000,\n  stale_inclusive: true,\n};\n","helpers.ts":"export function cmp(a: any, b: any): number {\n  if (a < b) return -1;\n  if (a > b) return 1;\n  return 0;\n}\n","core.ts":"import {cmp} from './helpers.ts';\n\nexport function run(payload: any, policy: any): any {\n  const now = payload.now;\n  const jobs: any[] = payload.jobs || [];\n  const m = new Map<any, {j: any; i: number}>();\n  jobs.forEach((j, i) => {\n    m.set(j[policy.dedup_key], {j, i});\n  });\n  const items = Array.from(m.values()).sort((a, b) => a.i - b.i);\n  const ready: any[] = [];\n  const waiting: any[] = [];\n  const dead: any[] = [];\n  const cancelled: any[] = [];\n  for (const {j, i} of items) {\n    const code = j.last_code || 0;\n    const retryable = policy.retryable_codes.includes(code);\n    if (j.cancelled && (policy.cancel_wins || !retryable)) {\n      cancelled.push({j, i});\n      continue;\n    }\n    const age = now - j.created;\n    const stale = policy.stale_inclusive ? age >= policy.stale_after : age > policy.stale_after;\n    if (stale || j.attempt >= policy.max_attempts || (code !== 0 && !retryable)) {\n      dead.push({j, i});\n      continue;\n    }\n    let due = j.due || 0;\n    if (retryable) {\n      const factor = policy.exponential ? Math.pow(2, Math.max(0, j.attempt - 1)) : 1;\n      const delay = Math.min(policy.base_delay * factor, policy.delay_cap);\n      due = Math.max(due, (j.failed_at || 0) + delay);\n    }\n    if (due > now) waiting.push({j, i, due});\n    else ready.push({j, i});\n  }\n  ready.sort((a, b) => {\n    let c = cmp(a.j.priority, b.j.priority);\n    if (policy.priority_high_first) c = -c;\n    if (c) return c;\n    c = cmp(a.j.created, b.j.created);\n    if (!policy.fifo_ties) c = -c;\n    if (c) return c;\n    return a.i - b.i;\n  });\n  if (policy.dead_order === 'id') {\n    dead.sort((a, b) => cmp(a.j.id, b.j.id) || a.i - b.i);\n  } else {\n    dead.sort((a, b) => a.i - b.i);\n  }\n  return {\n    ready: ready.map((x) => x.j.id),\n    waiting: waiting.map((x) => ({id: x.j.id, at: x.due})),\n    dead: dead.map((x) => x.j.id),\n    cancelled: cancelled.map((x) => x.j.id),\n  };\n}\n"}}
```