**Current requirements**

- `plan(rows, budget_cents, excluded_channels=())` returns exactly `selected_ids`, `spent_cents`, `remaining_cents`, and `skipped_ids`.
- `budget_cents` must be a nonnegative integer; booleans and other non-integers raise `ValueError`. Inputs must not be mutated, and floating-point arithmetic must not be used for money.
- Only the first occurrence of each `id` is considered. Deduplication occurs before archived filtering, channel exclusion, cost validation, and budgeting.
- Rows with `archived` true are excluded; missing `archived` means false.
- Channel exclusions compare `strip().casefold()` values. A missing channel is `""`; an empty exclusion list excludes nothing.
- Eligible rows are traversed by descending integer `priority`; missing priority is `0`, and ties retain input order.
- A row is selected when its cost is at most the remaining budget. Otherwise, it is added to `skipped_ids` and traversal continues. Zero-cost rows may be selected.
- If `price` is present, it must be a finite, nonnegative decimal string and is converted to cents using `Decimal` with `ROUND_HALF_UP` per row. Legacy rows use integer `cost_cents`. Costs are validated only for eligible rows; invalid eligible costs raise `ValueError`.

**Explicitly superseded requirement**

The original input-order traversal was replaced by descending-priority traversal.

**Specific cases**

- If the first row for an `id` is archived, that `id` is consumed: later duplicates are ignored entirely.
- If the budget equals a proposal’s cost, it is selected and the remaining budget becomes zero.
- If both `price` and `cost_cents` are supplied, `price` takes precedence and `cost_cents` is ignored.