### 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 types raise `ValueError`.
- Deduplication occurs first: only the first occurrence of each `id` is considered.
- Archived rows are excluded; a missing `archived` value means `False`.
- Channel exclusions occur after deduplication. Channels and exclusions are normalized with `strip().casefold()`; a missing channel is `""`, and no exclusions exclude nothing.
- Eligible costs are validated before selection. Rows with `price` use that finite, nonnegative decimal-dollar string, converted individually to cents with `Decimal` and `ROUND_HALF_UP`. Legacy rows require a nonnegative integer `cost_cents`; invalid values—including booleans—raise `ValueError`. Invalid costs on duplicate, archived, or excluded rows are ignored.
- Eligible rows are selected greedily while their cost does not exceed the remaining budget. Too-expensive eligible rows are recorded in `skipped_ids`.
- Inputs are not mutated, and floating-point arithmetic is not used for money.

**Superseded requirement:** Original input-order traversal was replaced by descending `priority` order, with missing priority treated as zero and equal priorities retaining input order.

**Specific cases:**

- If the first row for an `id` is archived, it claims that `id`; later duplicates are ignored completely.
- If cost equals the remaining budget, the proposal is selected and the remaining budget becomes zero.
- If both `price` and `cost_cents` are supplied, `price` takes precedence.