review.md
Rama main · réplica de lectura del repositorio original.
# Review process
How a change gets from written to merged. Written from what has gone wrong here.
Nothing loads this file automatically. Every review brief cites it by absolute
path, and `security-reviewer` reads it as its first act. The four
non-negotiables below are also inline in `.claude/agents/security-reviewer.md`
so a reviewer that never opens this file still has them; **this file is
authoritative** if the two ever disagree.
## Who reviews what
Route the work, then route the review to someone who did not write it.
| Change touches | Review |
|---|---|
| RLS, tenant scoping, auth, crypto, the agent command protocol | `security-reviewer`, mandatory |
| Deletes, GC, object-storage reclamation, run-locks, advisory locks | `security-reviewer`; its job is to find data that gets deleted and should not |
| A migration that drops, alters or cascades | `security-reviewer`, plus a stated converge path for databases on the earlier version |
| Anything irreversible | Never self-approved |
**Severity is judged by consequence, never by provenance.** "The already-shipped
sibling has the same shape" is a scope note for the owner, not a downgrade. That
reasoning let a reproduced, irreversible data-loss race through here; a review
bot had to catch it, and it was fixed only after the owner overruled the call.
Reviewers cost real time, so scope beats volume: one well-scoped reviewer beats
three vague ones.
**Flag only gaps that affect correctness or the stated requirements.** A
reviewer asked to find gaps will find some whether or not they exist, and
chasing every one produces defensive code and tests for cases that cannot
happen. Say in the brief what would *not* count as a finding.
## Scope the review before launching it
1. **The diff.** A commit range or an explicit file list. Not "the changes".
2. **The question.** What would count as a finding, and what would not.
3. **The budget.** The files to read, the commands to run, what is explicitly
out of scope ("do not audit CI history").
4. **The stop condition.** What the reviewer returns when it finds nothing, and
the instruction that if it needs something outside the budget it **reports
that it needs it and stops** rather than going to get it.
5. **The role to connect as**, for anything touching RLS, tenancy or deletion.
**Review a committed range, never a live working tree.** A reviewer here watched
a concurrent process revert a file mid-review and had to rebuild an isolated
copy from `HEAD`. If the work is not committed, say so and stop. Reviewers run
with `isolation: worktree` for this reason.
**Commit before the slow suite.** `make test-integration` is about
nine minutes locally. A reviewer interrupted mid-suite with uncommitted notes
loses all of them.
**Never wait with an unbounded loop.** `until … sleep` against a build or a CI
run is what got most killed runs here killed. Nothing denies that shape any
more, so this one is on you: poll a bounded number of times, or run the wait
with an explicit timeout, and report where it got to when the budget runs out.
## The four non-negotiables
**1. Does it break something that currently works?** Construct the honest cases
and run them. This matters as much as the bug: three of the four holes in the
version guard were over-firing or silent-skipping, not under-detection.
**2. Do the tests actually test it?** Delete or invert the thing the test
protects, watch it go red, restore it, watch it go green. **Paste both outputs
and the commands that produced them.** A review that says "the tests cover this"
has not happened. This is the check that catches what this repo actually ships -
not tests that fail, but tests that pass for the wrong reason:
- a frontend test asserting three server error codes the server never returns;
- m112's RLS proofs, which opened their own connections and so never went
through the dispatch that sets the scope, every test green, every policy
inert;
- a schema guard that read the first substring match instead of the
`CREATE TABLE`, and passed with both cascading foreign keys planted;
- a negative control recorded with `t.Logf` inside an `if`, so it could not
fail.
**3. What does this check print, and what does it exit, when its input is
missing, its command fails, or its pattern matches nothing?** Empty command
substitution, a loop over nothing, a grep that matches nothing, a CI guard that
failed open when `git rev-list` errored, each has shipped here as a guard
announcing success over its own errors.
**4. Run it as the real role.** This is the highest-yield technique in this
repository and nothing that only reads code substitutes for it. Connect as the
provisioned `NOSUPERUSER NOBYPASSRLS` application role, go through the
repository layer, and execute the statement you are about to publish. It is how
a documented operator recovery statement was found to be impossible on a
`FORCE ROW LEVEL SECURITY` table for every real install, after passing in every
test that ran as superuser.
## Numbers in the review
Every count the reviewer states (tests, releases, rows, call sites, policies)
is recounted **by the reviewer, with a command, in the review**. Never repeated
from the diff, the PR body, or earlier in the session. Four wrong figures
shipped here in one week, each inherited rather than counted.
## Bot reviews
CodeRabbit, Qodo and Greptile review most PRs. They have caught things
adversarial review missed, including the highest-severity finding of one week,
and they have been confidently wrong.
- **Read every finding and check it.**
- **Never apply a bot patch unread.** A committable suggestion here contained
`+$conn_entries;` instead of `++$conn_entries;`, a no-op that would have
closed the finding while fixing nothing. Take the reasoning; write the fix.
- **Close each thread with a comment naming the commit that addressed it, or the
reason it is wrong.** An unanswered resolve records nothing.
- **Wait for the bots before merging.** Most PRs here merge in under ten
minutes, which is faster than the bots post. Either the findings are worth
reading or the bots are worth turning off.
## Before merge
- `ci.yml` green, and green again after the merge commit.
- `make test-integration` locally for any diff touching RLS, tenant scoping, the
email domain, or reclamation: CI does not run that package.
- `make check-versions` for anything that moves a version surface.
- For anything under `.claude/`, a human read of the whole diff. There is no
lint and no test suite over the agent definitions or the settings, so a broken
one fails silently, at dispatch time, in someone else's session.
- The mutation performed by hand, both outputs pasted.
- Every number recounted, every behavioural claim executed.
- Every bot thread read and answered.
## After merge
Merge, release, deploy every affected layer, **verify against the running
system**, and only then reply to the issue. Details in `CLAUDE.md`.
Default merge is a merge commit. Squash only when the branch carries a commit
that must not survive (a scratch commit that deliberately widens an RLS policy
to prove a guard catches it, for example), and verify afterwards that it is
unreachable from `main`.