Commit Graph

2 Commits

Author SHA1 Message Date
gsxdsm
63e1f81244 fix(gate): a dropped SQL-literal count tightens the baseline instead of failing the gate (#2888)
## Why

`check-sql-column-literals` runs inside `pnpm test:gate` — the
**blocking** lane. It hard-fails when a count *drops*, so a single
converting PR that doesn't re-record takes down the gate for **every
worker in the program** until someone fixes the baseline by hand.

That is not hypothetical. It is happening on `main` right now
(`team-analytics.ts` 6 → 3, fixed by #2880), and it is the **second**
instance of the shape — the lifecycle census hit it from a merge wave
that dropped eleven files at once.

## The census already resolved this exact trade-off

From `docs/testing.md`, on why the census stopped hard-failing on a
drop:

> "the drop is almost never the failing author's to fix ... A
permanently-red gate is a bigger hole than a stale allowance, because it
gets ignored and then nothing is guarded at all."

That reasoning applies here **with more force**, because the census is
*not* in the blocking lane and this check *is*. Same failure mode,
higher cost, opposite policy — this aligns them.

## What changes

A drop now rewrites the baseline downward, reports what it lowered, and
exits 0:

```
[check-sql-column-literals] baseline TIGHTENED — fewer literals than it allowed

  packages/core/src/team-analytics.ts: allowed 6, now 3

The baseline has been rewritten downward. COMMIT IT so the allowance cannot be regrown into;
in CI this write is discarded with the runner, which is why the gate is green and not silent.
```

**The rise check is untouched.** "No new SQL column literals" is the
ratchet's actual purpose and still fails hard.

The stale-allowance concern the old comment raised is real and is
preserved: the rewritten file must be committed, and in CI the write is
discarded with the runner — so the gate goes green rather than silently
passing a stale allowance, exactly as the census does.

## Verified in both directions

| scenario | result |
|---|---|
| drop (`team-analytics.ts` 6 → 3, the live case) | **tightens, exit 0**
|
| rise (a literal added to a zero-allowance file) | **fails, exit 1** —
`task-age-staleness.ts: 1 SQL column literal(s), baseline allows 0` |

The rise probe needed a zero-allowance file: adding one literal to
`team-analytics.ts` keeps it at 4 against an allowance of 6, which is
correctly *not* a rise. Worth noting because it is an easy way to
conclude the guard is dead when it is working.

## Relationship to #2880

#2880 fixes the **instance** — it re-records the current drift so the
gate goes green now. This fixes the **class**, so the next conversion
doesn't take the gate down again. They are independent and either can
land first; if #2880 lands first, this becomes a no-op on a matching
baseline.

## Verification

- `pnpm test:gate` — exit 0 with this change applied
- `pnpm lint` — clean

No changeset: gate tooling, not published behaviour.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-30 17:40:17 -07:00
gsxdsm
ef50244234 feat(gate): freeze the SQL column-literal surface — 30 sites, none may be added (#2841)
Instruments a surface no existing check can see. Follows #2839, and
**corrects the count I reported there** (12 → 14).

## Why it was invisible

The lifecycle census parses TypeScript **comparisons**; a legacy id
inside a SQL string is string data. The inert-seam gate reasons about
parameters and call sites. Neither has ever looked here.

**What it cost:** `cleanupStaleMergeQueueRowsImpl` filtered on `t.column
!= 'in-review'`, so on a renamed board every queued card looked stale,
its `merge_queue` row was deleted, and the card became **unleaseable**.
The operator found it reviewing #2819 — in SQL I had already read past
during that same work.

The quieter half is analytics: five sites count `"column" = 'done'`, so
throughput, cycle time, and team dashboards report **zero completed
work** on a renamed board. Nothing errors, which is why nobody files it.

## What this does, and does not do

It does **not** fix the sites. `resolveProjectColumnsForRoles` is the
mechanism and its migration has an owner (#2839). This freezes the
population so the surface cannot grow underneath that migration: a new
file or a higher count fails, **and a lower count fails too** — so the
baseline ratchets down as sites migrate rather than leaving slots to
silently regrow into. That is the same rot as an allow-list entry for a
deleted function, which this repo already hit once.

AST-based, deliberately: a line grep for the same pattern reports **37**
hits, **25 of them prose** quoting `column === "done"` in explanatory
notes. A guard that is 68% false positives trains its readers to skip it
— a lesson this program has already paid for.

## Two corrections found by mutation-testing my own gate

**1. Clause fragments were missed.** Requiring a SQL keyword *in the
same literal* skipped `team-analytics.ts`, which builds
`["assignedAgentId IS NOT NULL", `"column" = 'done'`, ...]` and joins
them into a `WHERE` later. That fragment is as vocabulary-bound as any
full query but contains no keyword. Fixing it took the population **12 →
14**, so the number I put on #2839 was low.

**2. My first mutation test proved a direction it had not.** I replaced
the first textual occurrence in a file — which was inside a **comment**
— and read the unchanged count as the scanner being broken. The scanner
was right; my test was wrong. All three directions are now driven
against real SQL:

| mutation | result |
|---|---|
| add a full query with a legacy comparison | `3 SQL column literal(s),
baseline allows 2` |
| add a bare clause **fragment** (no keyword) | caught — same failure |
| migrate one away (count drops) | `1 site(s) now, baseline still allows
2 — re-record it` |
| restore | exit 0 |

I am flagging that second one because it is the exact failure mode this
program keeps finding: a green result read as evidence when the
experiment was invalid.

## Verification

`pnpm test:gate` green with the new check in it · lint 0 · single AST
pass. Wired into `test:gate` and both `pretest` hooks.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

* **Bug Fixes**
* Added automated checks to detect increases in legacy SQL column
literals.
* Added baseline tracking to ensure known SQL literal counts do not
regress.

* **Tests**
* Expanded pre-test and gated verification steps with SQL literal and
mock completeness checks.
  * Updated test validation workflows to enforce the new safeguards.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-30 16:06:24 -07:00