docs(core): the archived LANE-vs-STATE triage, done — 8 SQL sites classified with evidence (#3154)

#3147 scoped this cluster and said the first question is *"which of
these are LANE questions and which are STATE markers?"* — and that
nobody had answered it. **This answers it** for the Drizzle half, per
site, by reading what each query is for.

I claimed it because it has sat unclaimed for many rounds and `--claims`
reports `AVAILABLE: 0 files / 0 guards` — this is the only real work
left in the area. Nothing is converted here.

## LANE (6) — must resolve a renamed archive lane

| site | evidence |
|---|---|
| `store.ts` revert lookup | `ne(archived)` + `ne(done)` picking
**live** revert candidates |
| `branch-group-ops.ts:82` | near-duplicate marker cleanup over **live**
rows |
| `branch-and-pr-entities.ts:438` | content-fingerprint duplicate guard,
gated on `!includeArchived` |
| `branch-and-pr-entities.ts:470` | recent **sibling** lookup |
| `async-lifecycle.ts:68` | `liveLineageChildFilter` — the name is the
classification |
| `async-search.ts:82` | `liveSearchPredicate(includeArchived)` — same |

Four already hold `store` / `this.asyncLayer`. The two predicate
builders need one optional parameter each — the shape used throughout
this program.

## STATE (2) — converting these would be a **bug**

**`task-mutation-ops.ts:1072`** — `cleanupArchivedTasksImpl` selects
`eq(column, "archived")` and then `rm`s each row's files. Widening it to
the resolved archived set would feed cards **merely resting in a board's
archive lane** into a filesystem delete.

This is the most destructive site in the family, and it **looks
identical to the LANE sites at a glance** — same column, same operator,
same file neighbourhood. That is the whole argument for triaging before
converting.

**`async-self-healing.ts:61`** — soft-deleted rows whose column
*drifted* from the archive marker (`isNotNull(deletedAt) && ne(column,
"archived")`). Resolving it would classify a soft-deleted row sitting in
a renamed archive lane as drift and "repair" it.

## The raw-SQL half is already partly triaged in place

`async-maintenance.ts` is marked DELIBERATE-LITERAL as a STATE marker,
and `async-archive-lineage.ts`'s soft-delete path writes `column =
'archived', deleted_at IS NOT NULL` as the storage state it has just set
— STATE by construction.

## What this changes about the decision

Roughly **three quarters LANE, one quarter STATE** — and the STATE sites
are the ones that destroy data if converted.

That is why "convert all three encodings" cannot be a sweep, and why the
raw count of 52 made it look larger than it is: there are fewer sites to
convert than the headline, and the ones that must **not** be touched are
the part worth being careful about.

## Not converted here, deliberately

The gate requires all three encodings to move together, so the
conversion is one coordinated change with its inventories updated in the
same commit. This supplies the classification that change needs without
pre-empting it — and without me making a 52-site coordinated change at
the tail of a long session, which is exactly when I have made my worst
calls today.

## Measured

- Comment-only; parity test **2/2**.
- `tsc --noEmit -p packages/core` clean; census `--strict` clean. **No
census movement.**

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
gsxdsm
2026-07-31 06:43:18 -07:00
committed by GitHub
parent a8dae03fdb
commit 110d6fd150

View File

@@ -39,6 +39,80 @@ deliberate, are both real decisions with real blast radius. Neither is a fleet c
templates are worse again: one of them is a hand-written `SELECT` string, so its comparison is not even
a Drizzle expression that could take a bound value without rewriting the query.
FNXC:WorkflowResolvedColumns 2026-07-31-23:59 (THE TS HALF — mostly already converted; the inventory
counts FALLBACK ARMS, which is why 20 reads as 20 outstanding guards and is not):
Sampled the TS inventory the same way. It does not decompose into LANE/STATE the way the SQL half
does, because most entries are not pending conversions at all:
FALLBACK ARM of an already-converted guard — the literal is reached only when a caller supplies no
resolved set, and it is the documented degraded answer:
async-comments-attachments.ts `archivedColumns ? has(row.column) : row.column === "archived"`
— already marked DELIBERATE-LITERAL / FALLBACK ARM in place.
update-task-deps.ts:406-408 resolved arm first (`lifecycle?.archived ?? "archived"`), literal
last.
task-merge.ts:478,489 `if (!columns) return dependency.column === "done" || ...` — the
whole branch is the no-metadata fallback.
STATE / SENTINEL, not a lane:
task-id-integrity.ts:444 compares `getLiveTaskColumn`'s MANUFACTURED "archived", which that
function returns for archived OR SOFT-DELETED rows. A normalized
sentinel; resolving it would compare a lane id against a value no
lane produces.
archive-lifecycle-2.ts:47 `column: "archived"` is a WRITE — it SETS the archive state.
So the TS count overstates outstanding work in the OPPOSITE direction from the SQL count: the SQL half
had two sites that must never be converted, and the TS half has several that are already correct.
WHAT THIS MEANS FOR THE DECISION. "52 sites across three encodings" is the number the gate must keep
in LOCKSTEP, not the number a conversion has to CHANGE. After triage the conversion is the six LANE
Drizzle sites plus whatever small TS remainder is neither a fallback arm nor a sentinel — with the
inventories updated in the same commit so the three encodings stay in step.
That is a materially smaller and better-understood change than the headline implies, and it is now
specified rather than estimated. Still not done here: the gate requires one coordinated commit, and
the remaining judgement is per-site verification of the TS remainder, which wants the owner making the
conversion rather than a third pass of sampling.
FNXC:WorkflowResolvedColumns 2026-07-31-23:59 (THE TRIAGE, DONE — 8 SQL sites classified with evidence):
The scoping note below says the first question is "which of these are LANE questions and which are
STATE markers?" and that nobody had answered it. Answered here for the Drizzle half, per site, by
reading what each query is FOR. Nothing is converted; this is the input the conversion needs.
LANE (6) — these select or exclude LIVE work, so a renamed archive lane must be resolved:
store.ts revert lookup: ne(archived) + ne(done) picking live revert candidates.
branch-group-ops.ts:82 near-duplicate marker cleanup over live rows: ne(archived) + ne(done).
branch-and-pr-entities:438 content-fingerprint duplicate guard, gated on `!includeArchived`.
branch-and-pr-entities:470 recent sibling lookup: ne(archived) + ne(done).
async-lifecycle.ts:68 `liveLineageChildFilter` — the name is the classification.
async-search.ts:82 `liveSearchPredicate(includeArchived)` — same.
Four of these already hold `store`/`this.asyncLayer`; the two predicate builders need one
optional parameter each, the shape used throughout this program.
STATE (2) — these are ABOUT the marker `archiveTask` writes, and converting them would be a BUG:
task-mutation-ops.ts:1072 `cleanupArchivedTasksImpl` selects eq(column,"archived") and then `rm`s
each row's files. Widening to the resolved archived set would feed cards
merely RESTING in a board's archive lane into a filesystem delete. This
is the most destructive site in the family and it looks identical to the
LANE ones at a glance — same column, same operator.
async-self-healing.ts:61 soft-deleted rows whose column DRIFTED from the archive marker
(`isNotNull(deletedAt) && ne(column,"archived")`). Resolving it would
classify a soft-deleted row sitting in a renamed archive lane as drift
and "repair" it.
The raw-SQL half is already partly triaged in place: `async-maintenance.ts` is marked
DELIBERATE-LITERAL as a STATE marker, and `async-archive-lineage.ts`'s soft-delete path writes
`column = 'archived', deleted_at IS NOT NULL` as the storage state it has just set — STATE by
construction.
SO THE SHAPE OF THE WORK: roughly three quarters LANE, one quarter STATE, and the STATE sites are the
ones that destroy data if converted. That is why "convert all three encodings" cannot be done as a
sweep, and why the count alone made it look bigger than it is — the number to convert is smaller than
52, and the number that must NOT be touched is the part worth being careful about.
NOT CONVERTED HERE, and deliberately: the gate requires all three encodings to move together, so a
conversion is one coordinated change with its inventories updated in the same commit. This supplies
the classification that change needs; it does not pre-empt it.
FNXC:WorkflowResolvedColumns 2026-07-31-23:50 (one wrong reason for picking the cheap option, removed):
The second option looks like it has already been taken — `trait-types.ts` annotates the flag
"RESTRICTED (built-in only)", which reads as "a custom board cannot have its own archive lane". It does