**Replaces #3085, which I am closing.** #3057 landed the same
cold-storage conversion while that PR was open. Rather than argue about
which spelling wins, this keeps only what `main` does not have: the
coverage, and two audits.
## #3057 converted this and shipped no test
`listTasksImpl`'s `columnFilterIsArchive` replaced `columnFilter ===
"archived"`. Correct change — and the kind that needs a test more than
most, because of how it fails.
Archived rows do not live in `tasks`; `archiveTask` copies them into the
archive store and removes them. This decision is whether that second
store is read **at all**. Against the literal, a caller naming a renamed
archive lane — `listTasks({ column: "filed", includeArchived: true })`,
which is what an archive view does — got an empty page from the only API
that can reach those rows.
Note the shape: the **unfiltered** read (`!columnFilter`) was always
correct. It fails only for the caller that names the lane, so it
survives any board-level smoke test and presents as *"the archive is
empty"* rather than as a bug. That is precisely the class that regresses
quietly once the conversion that fixed it has nothing holding it.
Three cases, and each earns its place:
| case | what it stops |
|---|---|
| renamed archive lane | the regression itself |
| legacy `archived` id (**control**) | a future conversion that resolves
the renamed lane and *drops* the legacy seed — the seeding hazard in its
other direction |
| non-archive lane (**negative**) | the widening turning every filtered
board read into a second-store round-trip |
**MUTATION**: restoring `columnFilter === "archived"` fails **only** the
renamed case.
**A trap worth recording.** My first version left the LIVE read real
against a fake `layer.db`. It threw, the `.catch` swallowed it, and all
three cases passed the negative — *including the legacy control*, which
is what exposed it. A test whose subject is never reached looks
identical to one whose subject answered no. `readLiveTaskRows` is mocked
now, and the control is what made it detectable.
## Audited, not converted — two dead sync paths
Both would be real defects if they ran. Neither runs.
| site | why it is dead |
|---|---|
| `mission-store.ts` (feature-delete link check) | `getMissionStoreImpl`
returns the AsyncDataLayer-backed `AsyncMissionStore` under PostgreSQL;
the sync `MissionStore` reached via `this.db.prepare` is legacy SQLite
only |
| `lifecycle-ops.ts` (polling-replica archive emit) |
`checkForChangesImpl` opens with `store.db.getLastModified()` /
`store.db.prepare`, which throw in backend mode |
The second is the sharper one: **both** its guard and the `to:
"archived"` it emits are literals, so a polling replica on a renamed
board would emit a move to a column the board does not declare.
Recorded in place — the treatment `project-store-ops.ts`'s dequeue twin
already has — so the census entries are not mistaken for unconverted
debt, and whoever deletes the sync SQLite residue takes these with it.
**They stay COUNTED.** Marking them DELIBERATE-LITERAL would buy a
smaller number by asserting the code is *correct*. It is not correct; it
is unreachable. Those are different claims with different expiries, and
the census should keep pointing here until the code is gone.
## Census
No movement — by design. This PR adds coverage and audits; it converts
nothing that was not already converted on `main`.
## Measured
- 3 new cases pass; `src/__tests__/{cold-storage,archive,unarchive}*` —
**6 files / 21 tests pass**
- `tsc --noEmit -p packages/core` clean; census `--strict` clean
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>