### Current requirements
- Python 3.12 standard library only; public signature: `plan(rows, budget_cents, excluded_channels=())`.
- Never mutate `rows` or `excluded_channels`; use no floating-point arithmetic for money.
- Budget must be a nonnegative integer. Invalid budgets, including booleans, raise `ValueError`.
- Each row has a string `id`. Only its first occurrence represents that id, determined before filtering or ordering.
- Filter archived rows (missing `archived` means false) and excluded channels. Normalize channels and exclusions with `strip().casefold()`; missing channel means `""`. Exclusions are a list or tuple of strings; empty exclusions exclude nothing.
- Traverse eligible proposals by descending integer priority, defaulting to zero. Equal priorities retain input order.
- Validate every eligible row’s cost before budget selection. Invalid costs in duplicate, archived, or excluded rows are ignored.
- If `price` exists, require a finite nonnegative decimal string in dollars, allowing surrounding whitespace. Convert each row individually using `Decimal` and `ROUND_HALF_UP`.
- Otherwise require nonnegative integer `cost_cents`; missing values and booleans are invalid. Invalid costs raise `ValueError`.
- Select affordable proposals, including zero-cost ones; skip unaffordable proposals and continue.
- Return exactly `selected_ids`, `spent_cents`, `remaining_cents`, and `skipped_ids`. Both id lists follow traversal order; skipped ids include only unaffordable eligible proposals. Empty input spends zero.

### Superseded requirement and cases
Descending-priority traversal explicitly superseded original input-order traversal.

An archived first occurrence blocks all later duplicates. Exact-budget cost is selected, leaving zero. When both cost fields exist, `price` takes precedence; `cost_cents` is ignored.