Skip to content

fix(admin): three information-loss boundaries — one fixed, one disproven, one registered - #1679

Merged
njrini99-code merged 2 commits into
mainfrom
agent/mc-producer-boundary
Aug 30, 2026
Merged

fix(admin): three information-loss boundaries — one fixed, one disproven, one registered#1679
njrini99-code merged 2 commits into
mainfrom
agent/mc-producer-boundary

Conversation

@njrini99-code

@njrini99-code njrini99-code commented Aug 30, 2026

Copy link
Copy Markdown
Owner

The product-diagnostics tranche

Phases 1–3 of the closeout plan, on one branch because the plan merges them as
one. Three findings: one real defect fixed, one hypothesis disproven with
evidence
, one known risk moved out of a code comment and into the registry.


1 — MC-PRODUCER: an unreadable resolution ledger is not an unresolved incident

fetchResolutions returned an empty Map for both "healthy, nothing
resolved" and "couldn't read the ledger" — proven identical by a red test.

The obvious fix would have been worse. The sibling arm already models this
(fetchRepairPrs -> { byIncident, readable, reason }) and
IncidentResolution.resolvedBy is even typed 'auto' | 'manual' | 'unknown'
with nothing in the codebase ever producing 'unknown'. So the first draft
asserted the incident should carry it. Then LifecycleSpine:

closeState = regressed ? 'failed' : incident.resolution !== null ? 'proven' : 'not-reached'

A non-null resolution for an unreadable ledger would render PROVEN CLOSED
upgrading "we could not read this" into "this is fixed".

Shipped instead: the producer carries readable/reason, and the app source
degrades to partial — a state already in the union. The app source had been
hardcoded reading on the reasoning that app events throw when unreadable.
True of events; false of the soft-failing ledger.

A third thing the tests forced: describeBlindness only named blind sources,
so a partial degradation was recorded and reported nowhere.


2 — Category drift: disproven

The plan said to reproduce it with one real value, and if the real value passes,
keep tracing rather than change mapping code. It passes.

31 distinct rca_analysis openings read out of production and run through the
real matcher — all land correctly, across both em-dash and double-hyphen
spellings. Five "No fix needed…" variants stay uncategorized, which is
correct: they're genuinely ambiguous between ALREADY FIXED and NOT A DEFECT,
and those carry different resolve evidence.

One row worth naming: an actionable finding with a file and line number sits in
uncategorized because it doesn't open with FIX HERE. That's a routine-prompt
contract problem, not a mapping problem — recorded in the test so nobody "fixes"
the matcher by teaching it to guess.

Two ExpenseCategory unions look like drift and aren't. Golf's equals the
golf_expense_category enum exactly; baseball's equals its own CHECK constraint
exactly. Two products, two constraints. But nothing enforced that — and
getExpenseSummary skips unknown categories from the breakdown while still
counting them in total, so the breakdown silently stops adding up. Unreachable
only because of a database constraint no test mentioned. Now pinned.


3 — Capture audit

Not exhaustive, and the plan says it shouldn't be — 128 const { data } = await
sites exist. Audited the path whose whole job is reporting whether production is
healthy.

Site Verdict
reliability/collect.ts SAFE — rejection becomes status: 'blind' + reason
rollup-b.ts SAFE — returns { value: empty, degraded: true }
team-page-extras.ts INFORMATION_LOSS — registered as a gap
incident-feed.ts INFORMATION_LOSS — fixed here

Fixed. queryPriorResolutions was const { data } = await … — the error
discarded entirely. The comment directly above it already warned that "the
swallowed error causes regression tags to silently disappear — exactly when
operators most need them"
, and then chunking was added. Chunking removed the
URL-length cause; it did nothing about the error being thrown away.
Absent
regression tags read as "nothing regressed", on the board you open to find out
whether something regressed.

Registered, not patched. team-page-extras.ts resolves a rejected error
count to 0, and 0 grades the team 'A' — a read failure rendering as a
healthy team. Already known in a code comment, which is precisely the problem: a
risk that lives only in a comment is invisible to everything that checks. Fixing
it needs an "unknown" grade in computeTeamGrade — a product decision.


Two injections that did not land

The first two attempts to prove the regression-swallow test could fail both
reported "8 passed" — because if (error) { appears three times in that file
and the replacement hit line 307 instead of 373.

An injection that silently misses looks exactly like a test that cannot fail,
and it would have shipped an unverified test under a confident PR body. Targeted
by line: 2 failed, 6 passed.

Verified by the repo's own ratchets

audit:supabase-errors independently counted the fix — unchecked Supabase reads
1041 → 1040 — and required locking it into the baseline so it can't be
reverted unseen.

control-plane:verify failed on this branch until #1679 itself was given a
disposition — the verifier holding its own author to the rule that an
unclassified open PR is undefined background state.

Verified

preflight 0 · npm test 1267 files / 12071 passed · business 0 ·
control-plane:verify 0 (20 pass, 0 fail, 0 unknown, 5 acknowledged gaps)

🤖 Generated with Claude Code

https://claude.ai/code/session_01NbDxyygyXRUEERuGocpZZH

…incident

MC-PRODUCER. The hypothesis was that `fetchResolutions` loses information at its
I/O boundary. A red test proved it, and the test that proved it also proved the
obvious fix would have been worse.

THE DEFECT

`fetchResolutions` returned a bare `Map<fingerprint, row>`. On a Supabase error
it logged a warning and returned an EMPTY map — byte-identical to the map it
returns when the ledger is healthy and genuinely holds no rows. Its own comment
said the fail-soft was fine because "the lifecycle derivation handles" the
unknown. It could not: both paths produced `resolution: null`.

Measured with the real producer and a mocked ledger:

    healthy + no rows   -> incidents[0].resolution === null
    ledger unreadable   -> incidents[0].resolution === null      identical

THE FIX I DID NOT SHIP, AND WHY

The sibling arm of this same producer already models this correctly —
`fetchRepairPrs -> { byIncident, readable, reason }`, and `toRepair` returns
`status: 'unknown'` with an explaining note. `IncidentResolution.resolvedBy` is
even typed `'auto' | 'manual' | 'unknown'`, and measured before writing any
code: the only producer is `row.resolutionSource`, which is `'auto' | 'manual'`.
Nothing has ever produced `'unknown'`. The type knew about the state; the code
could not reach it.

So the first draft asserted the incident should carry
`resolution.resolvedBy === 'unknown'`. Then LifecycleSpine.tsx:

    closeState = regressed ? 'failed'
               : incident.resolution !== null ? 'proven'
               : 'not-reached'

A non-null resolution for an UNREADABLE ledger would have rendered the incident
as PROVEN CLOSED. That upgrades "we could not read this" into "this is fixed" —
strictly worse than the bug being fixed. The test was rewritten to the contract
that is actually safe, and says so where a future reader will find it.

WHAT SHIPPED

Two small changes, both using machinery that already existed.

1. `fetchResolutions` carries its readability, exactly like its sibling:
   `{ byFingerprint, readable, reason }`. The empty map is still returned — the
   board must not go down for this — but the caller is now told the difference.

2. The `app` source stops asserting more than it knows. It was hardcoded
   `health: 'reading'` on the reasoning that app EVENTS throw when unreadable,
   so reaching that line proves the event arm is healthy. True of events. False
   of the resolution ledger, which fails soft. When the ledger is unreadable the
   app source is now `partial` — a state that already exists for "reading one
   arm, blind on another" — carrying the reason.

No new type, no new source name, no UI change, and the incident is still not
claimed resolved.

A THIRD THING THE TESTS FORCED

`describeBlindness` only named sources whose health was `blind`, so a `partial`
source was recorded in the data and reported nowhere. A degradation nothing
surfaces is the same class of defect as one nothing records. It now names both,
worded differently on purpose: "could not be read" is a blackout, "read
incompletely" is a source still delivering some of its signal. Collapsing them
would trade one wrong claim for another.

PROVEN ABLE TO FAIL — three injections:

    revert the app-health degradation        -> 2 red
    fetchResolutions hides the error again   -> 2 red
    beacon stops naming partial sources      -> 1 red

ALSO IN THIS COMMIT

The control-plane verifier's `control-plane-suites` check spawns vitest. The
failure-injection suite invokes the verifier FROM a test, which nests vitest
inside vitest and made that check unreliable under full-suite load. It is now
skippable via HELM_CP_SKIP_NESTED_TESTS=1 — and reports UNKNOWN when skipped,
never PASS, so `--static` exits 2 rather than pretending. A check that did not
run is not a check that passed.

PR #1678 recorded as ACTIVE in the open-PR dispositions. It is from a
CONCURRENT session, not this one, and the runtime verifier is what surfaced it
— an unclassified open PR is undefined background state, which is the residue
class #1677 closed. Not touched: it edits gen-enforcement-inventory.mjs, which
this branch does not.

Verified: npm run preflight 0; npm test 1266 files / 12052 passed;
control-plane:verify 0; control-plane:verify:static 0; incidents suite 166/166.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NbDxyygyXRUEERuGocpZZH
@qodo-code-review

Copy link
Copy Markdown

ⓘ Qodo reviews are paused because your trial has ended. Ask your workspace admin to add credits to resume reviews. Manage billing

@supabase

supabase Bot commented Aug 30, 2026

Copy link
Copy Markdown

This pull request has been ignored for the connected project qmnssrrolpinvwjjnufo because there are no changes detected in supabase directory. You can change this behaviour in Project Integrations Settings ↗︎.


Preview Branches by Supabase.
Learn more about Supabase Branching ↗︎.

@vercel

vercel Bot commented Aug 30, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
helmv3 Ignored Ignored Preview Aug 30, 2026 3:36am

Request Review

@coderabbitai

coderabbitai Bot commented Aug 30, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 564d8213-9632-4628-8f5a-016ae778f3bb

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

…ot to exist

Phases 2 and 3 of the product-diagnostics tranche, on the same branch as the
MC-PRODUCER fix because the plan merges them as one.

CATEGORY DRIFT — DISPROVEN, WITH EVIDENCE

The plan said to reproduce it with one real value and, if the real value passes
through correctly, to keep tracing rather than change mapping code. It passes.

Read out of production 2026-08-30 and run through the real matcher: 31 distinct
`rca_analysis` openings, every one landing where it should. The four canonical
prefixes derive correctly through both em-dash and double-hyphen spellings.
Three legacy free-prose openings ("Already fixed…", "Already applied…") derive
to already-fixed. Five "No fix needed…" variants stay `uncategorized`, which is
CORRECT — they are genuinely ambiguous between ALREADY FIXED and NOT A DEFECT,
and those carry different resolve evidence, so guessing would be the
unknown -> healthy move the engineering OS forbids.

The one row worth naming: an actionable finding with a file and a line number
("Add `code: \"qualifier_closed\"` to the return at golf.ts:1770…") sits in
`uncategorized` because it does not open with FIX HERE. That is a routine-PROMPT
contract problem, not a mapping problem, and the test says so where a future
reader will find it — so nobody "fixes" the matcher by teaching it to guess.

Traced the other category layers too. Two `ExpenseCategory` unions exist and
look like drift: golf has `transportation` and `entry_fees`, baseball has
`transport` and neither. They are not drift. Golf's union equals the
`golf_expense_category` ENUM exactly; baseball's equals its own CHECK constraint
exactly. Two products, two constraints, both internally consistent.

But nothing enforced that. `getExpenseSummary` accumulates with
`if (expense.category in summary.byCategory)`, so a category outside the union
is skipped from the breakdown while still counting toward `total` — the
breakdown silently stops adding up. Unreachable today ONLY because of a database
constraint that no test mentioned. Now pinned: widen either constraint without
widening its union and it fails.

CAPTURE AUDIT

Not exhaustive, and the plan says it should not be — 128 `const { data } = await`
sites exist. The objective is places where an operational failure becomes
apparently-valid state, so this audited the path whose whole job is reporting
whether production is healthy.

  reliability/collect.ts   SAFE — a rejection becomes status 'blind' + reason
  rollup-b.ts              SAFE — returns { value: empty, degraded: true }
  team-page-extras.ts      INFORMATION_LOSS, already known, now REGISTERED
  incident-feed.ts         INFORMATION_LOSS, fixed here

THE ONE FIXED. `queryPriorResolutions` was `const { data } = await …` — the
error discarded entirely. The comment block directly above it already warned
that "the swallowed error causes regression tags to silently disappear — exactly
when operators most need them", and then chunking was added. Chunking removed
the URL-length CAUSE. It did nothing about the error being thrown away, so RLS,
a timeout or a dropped connection still produced a short map, and absent
regression tags read as "nothing regressed" — the healthier-than-reality
direction, on the board you open to find out if something regressed.

It now returns `{ byFingerprint, readable, reason }`, matching `fetchResolutions`
and `fetchRepairPrs` beside it. Still fail-soft — the partial map is returned,
the feed does not go down — but the caller is told.

THE ONE REGISTERED RATHER THAN PATCHED. `team-page-extras.ts` resolves a
rejected 7-day error count to 0, and 0 grades the team 'A'. So a READ FAILURE
renders as a healthy team. It was already known and reasoned in a code comment,
which is exactly the problem: a risk that lives only in a comment is invisible
to everything that checks. Fixing it needs `computeTeamGrade` to have an
"unknown" grade, which is a product decision about what an ungraded team looks
like. Now in the acknowledged-gap registry, printed on every verifier run.

TWO INJECTIONS THAT DID NOT LAND, AND WHY THAT MATTERS

The first two attempts to prove the regression-swallow test could fail both
reported "8 passed" — because `if (error) {` appears three times in that file
and the replacement hit line 307 instead of 373. An injection that silently
misses looks exactly like a test that cannot fail, and it would have shipped an
unverified test with a confident PR body. Targeted by line number, it goes red:
2 failed, 6 passed.

VERIFIED BY THE REPO'S OWN RATCHETS

`audit:supabase-errors` independently counted the fix: unchecked Supabase reads
1041 -> 1040, and required the improvement be locked into the baseline so it
cannot be reverted unseen. Baseline committed.

`control-plane:verify` failed on this branch until #1679 was given a disposition
— the verifier holding its own author to the rule that an unclassified open PR
is undefined background state.

Verified: npm run preflight 0; npm test 1267 files / 12071 passed; business 0;
control-plane:verify 0 (20 pass, 0 fail, 0 unknown, 5 acknowledged gaps).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NbDxyygyXRUEERuGocpZZH
@njrini99-code njrini99-code changed the title fix(incidents): an unreadable resolution ledger is not an unresolved incident fix(admin): three information-loss boundaries — one fixed, one disproven, one registered Aug 30, 2026
@njrini99-code
njrini99-code merged commit 5503a5a into main Aug 30, 2026
48 checks passed
@njrini99-code
njrini99-code deleted the agent/mc-producer-boundary branch August 30, 2026 03:46
njrini99-code added a commit that referenced this pull request Aug 30, 2026
…lsof's silence (#1683)

* fix(lifecycle): an OPEN PR's checkout needs its owner's consent, not lsof's silence

Reproduced 2026-08-30: `npm run worktrees:retire` removed a concurrent
session's checkout (agent/round-type-reclassify, PR #1681, OPEN). Every
mechanical signal said disposable — clean, tip identical to its pushed
remote, and no process whose cwd `lsof` could see. Nothing was lost, because
parking is defined to keep the branch and PARKABLE already required the tip
to match its remote. But the checkout had an owner and the tool could not
tell, so this is a classifier defect, not a slip.

The unsound step is reading silence as absence. `lsof +D` samples one
instant:

    hasLiveProcess === true    proof of activity        — a sound veto
    hasLiveProcess === false   NOT proof of inactivity  — an agent session
                               between two tool calls has no visible cwd

classifyWorktree's own header stated the defect as design — "parking does
NOT consult the PR at all" — so the comment is rewritten in the same commit.
A file whose prose contradicts its code is the failure this program exists
to remove.

WHAT CHANGED

New worktree verdict KEEP_PR_OWNER_INTENT_REQUIRED, deliberately neither
ACTIVE (nothing proved anyone is using it) nor UNKNOWN (the PR read fine;
what is missing is a decision). It is in REQUIRES_HUMAN_VERDICTS, so the
standing authorization already excludes it.

config/open-pr-dispositions.json gains `worktree_policy`, because the
disposition LABEL was being asked to imply an ACTION readers had to infer:

    KEEP                  never parked automatically
    PARK_IF_REPRODUCIBLE  park once clean and pushed; the branch stays

A missing row, an unrecognised policy, or a disposition of ACTIVE/UNKNOWN
all KEEP. #1659 — an open PR waiting on a physical-device test — is released
explicitly by its owner and stays parkable, which is the case the park/retire
split exists for.

The registry is now current state in BOTH directions. It carried ACTIVE rows
for #1623, #1638, #1679 and #1680 after they closed or merged; control-plane
-verify now fails on a stale row as well as a missing one, and on a
worktree_policy outside the vocabulary. That vocabulary check lives in the
verifier because the lifecycle tool must fail SAFE on a malformed entry
(unrecognised policy => KEEP) and would therefore never report one.

#1681's row is recorded ACTIVE/KEEP, transcribed from observable state — PR
OPEN, worktree checked out — by this session and not by its owner, who
should confirm it. Recorded rather than left blank because leaving it blank
is exactly what let --retire park it.

WHAT THIS DOES NOT COVER

A worktree whose branch has NO PR is still parked on reproducibility alone —
the same defect class, with no PR to key a disposition on. Requiring recorded
intent for every branch would make --park unable to act at all, which defeats
the reason parking exists. Registered as WORKTREE_PARK_NO_PR_OWNERSHIP rather
than quietly accepted.

VERIFIED

preflight 0 · 1269 files / 12,101 tests 0 · docs:check 0 · knowledge:check 0
· markdown ratchet 30515, unchanged · control-plane:verify VERIFIED exit 0
(was CONTROL FAILURE on open-pr-residue) · lifecycle suite 60 passed.

Failure injection: disabling the ownership gate turns 8 of them red,
including the end-to-end CLI case where --park refuses a fixture worktree
whose PR is OPEN with no disposition. The lib was restored from a hashed
backup and re-verified byte-identical.

Assumption stated once: this ran in the canonical checkout rather than a
worktree. HELM_MAX_MUTATION_WORKTREES=1 and the other session holds the only
mutation workspace, so new-worktree.sh refuses by design; AGENTS.md permits a
single session in canonical, and the concurrency hazard the worktree rule
addresses does not apply because the other session is isolated in its own.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NbDxyygyXRUEERuGocpZZH

* chore(dispositions): record this PR's own row — the new rule's first cost

Opening #1683 made control-plane:verify red on open-pr-residue immediately,
because the key set must now equal the live open-PR set exactly. That is the
chosen tradeoff, not a surprise: a PR number does not exist until the PR does,
so the row can only ever land in a follow-up commit.

KEEP rather than PARK_IF_REPRODUCIBLE: this work runs in the canonical
checkout, which classifyWorktree holds ACTIVE before consulting anything else.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NbDxyygyXRUEERuGocpZZH

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
njrini99-code added a commit that referenced this pull request Aug 30, 2026
The registry is CURRENT STATE, so a merge makes its own row stale — this is the
two-ended cost ADR-2026-08-30 records, paid on schedule. control-plane:verify
reported it as a CONTROL FAILURE the moment #1687 merged (open-pr-residue:
"disposition rows for PRs no longer open: #1687"), which is the check working:
a current-state registry that only failed on MISSING rows would have carried an
ACTIVE row for a merged PR indefinitely, exactly as this file did for #1623,
#1638, #1679 and #1680 before this run.

After: 3 open PRs (#1659, #1678, #1681), all owned elsewhere, all classified.
control-plane:verify VERIFIED, exit 0 — 21 pass, 0 fail, 0 unknown.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NbDxyygyXRUEERuGocpZZH
njrini99-code added a commit that referenced this pull request Aug 30, 2026
… red

I wrote, earlier in this run, that `Supabase Preview` "has been red on every
main commit for so long that it reads as background noise." That is wrong.

Measured across eight PRs (#1679, #1680, #1686#1691), its conclusion is
SKIPPED every time — never FAILURE. It is also not one of main's six required
contexts, so it could not block a merge even if it did fail. A check that never
runs is invisible in a way a red one is not, which makes the real situation
worse than the one I described, not better.

The skipping is legitimate: Supabase only builds a preview branch for a PR that
touches supabase/migrations/**, and most do not. Branching is enabled and works
— #1681 touches migrations and its preview branch reached FUNCTIONS_DEPLOYED
today.

What is genuinely unaddressed is one level down: the Supabase branch record for
`main` carries status MIGRATIONS_FAILED with created_at == updated_at ==
2026-07-03T21:11:11Z, so it has not been refreshed since it was written.
Whether that is a live verdict on today's migrations or a stale setup artifact
is NOT established — both readings fit the timestamps, and picking one would be
a guess. Recorded as unresolved rather than resolved in the wrong direction.

This changes the shape of the "make Supabase Preview green" task: the check is
inert, not failing, so the decision is whether an inert check should be
required, repaired, or removed — not how to turn it green.

One consequence worth having: a preview branch is an isolated project, NOT
production, which makes it the environment the plan's "do not use production
for destructive behavioral tests" rule points toward. With Docker down it is
the only remaining route for exercising the golf-history migration. Not taken
from here — provisioning one is a real resource on the owner's account.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NbDxyygyXRUEERuGocpZZH
njrini99-code added a commit that referenced this pull request Aug 30, 2026
…ify every unledgered migration (#1692)

* fix(golf): make the four notification fan-outs tolerate a null player user_id

Preparation for 20260819200000 (preserve golf history on account deletion),
which changes golf_players.user_id from NOT NULL to nullable so a deleted
account leaves its rounds, shots and holes intact instead of cascading them
away.

How the four sites were found, rather than guessed: the generated types are
built from production, where user_id is still NOT NULL, so tsc cannot see the
change yet. I hand-edited a disposable copy of the golf_players Row/Insert/
Update types to make user_id nullable, ran typecheck, and took the errors as
the complete compile-visible breakage list. Four, all the same shape — a
golf_players -> users notification fan-out that puts user_id straight into an
`in` list. The file was then restored and verified byte-identical by sha256.

These four are stragglers, not a new problem: golf.ts already does exactly
this filter in three other fan-outs (2869, 3459, 4793). The patch makes the
four match, using the same `.filter((id): id is string => Boolean(id))`.

Ships BEFORE the migration on purpose. user_id is NOT NULL in production
today, so the filter removes nothing and the change is a no-op — which is
what decouples this from the lockstep deploy the migration header warns about.

What else the audit found, and did not change:
- No code path anywhere sets golf_players.user_id to null. Verified across
  src/ and supabase/. The trigger's `old.user_id is not null and new.user_id
  is null` condition is therefore reachable only through the FK's ON DELETE
  SET NULL — no ordinary write can trip the anonymization by accident.
- Every column the trigger nulls (first_name, last_name, email, phone,
  avatar_url, hometown, state, high_school_name, graduation_year, gpa) is
  ALREADY nullable, so no rendering path needs a change.
- The two remaining user_id consumers tsc did not flag are ownership checks
  (round-drafts.ts:690, generate-review/route.ts:75). Both compare with
  !== / === against user.id, so a null denies. Fail-closed, correct.
- An anonymized player keeps its golf_team_members row and would still appear
  on an active roster with a blank name. That is a product decision, not a
  compile break, and is registered in the incident rather than decided here.

Verified: typecheck 0 against today's types AND 0 against the nullable types;
lint 0; the three notification tests pass (8/8).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NbDxyygyXRUEERuGocpZZH

* docs(migrations): classify all 42 unledgered migrations against the live catalog

Closes the open question inside MIGRATIONS_REPO_PRODUCTION_LEDGER_DIVERGENCE:
the gap recorded that the repo and production's ledger disagree, but not what
was actually missing.

Method, because the method is the finding. A ledger row is a claim about
history, not evidence about the schema — so each of the 42 files with no
production ledger row was parsed for the objects it DECLARES (tables, columns,
functions, policies, indexes, triggers, enum values, constraints) and those
objects were checked against the live catalog in bulk. Not filenames.

  22  applied — every declared object present
   5  partial
   2  unapplied
  11  UNKNOWN by construction — GRANT/REVOKE and data backfills leave no
      catalog object to look for, and "probably applied" is not a verdict
   2  self-declared no-ops

The headline: 20260819200000_preserve_golf_history_on_account_deletion is the
ONLY golf-facing migration with genuinely missing effects. 1 of its 5 objects
exists, and the FK that does exist is still confdeltype='c' (CASCADE).

Two things this corrected that a filename read would have got wrong:

- Three "absent" policy sets are name divergences, not missing RLS. Production
  covers the same table and verbs under its own names
  (baseball_event_acknowledgements_* vs baseball_event_acks_*). One of the
  three is genuinely short a DELETE policy; the other two are equivalent or a
  superset. Predicate equivalence is NOT claimed — only verb coverage was
  measured.
- All seven ncaa_division enum values and all four avatar storage policies read
  ABSENT until re-queried case-insensitively; the extractor lowercases SQL and
  production stores them uppercase/capitalised. Two migrations would have been
  reported as production gaps that are not. A case-folding bug in a classifier
  is indistinguishable from a missing object unless you go and look.

Also confirms the plan's hard stop empirically rather than assuming it:
`supabase db push` would propose all 42, not the one migration the owner
authorised. `db push --dry-run` is itself covered by the permissions.deny
prefix rule and was not attempted; `supabase migration list --linked` answers
the same question and is not denied.

The incident gains the measured blast radius (104 players, all 104 with a
linked user; 521 rounds / 36,943 shots / 9,162 holes — the migration header's
2026-08-18 baseline has grown by about half again), the application
compatibility audit, and an explicit note that the local exercise is BLOCKED:
Docker Desktop was launched but its daemon never came up this session.

Verified: docs:schema-drift 0, docs:path-drift 0, docs:inventory-check 0,
markdown ratchet at baseline (net 0).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NbDxyygyXRUEERuGocpZZH

* fix(announcements): do not report "nobody to notify" as a delivery failure

Follow-on to the nullable-user_id compatibility patch. The recipient guard
fires on `userRows.length === 0` and logs a delivery FAILURE. After
20260819200000 a batch whose targeted players are all anonymized legitimately
resolves to zero users — nobody to notify is not a failed lookup, and logging
it as one is the failure-vs-empty conflation this repo keeps removing, pointed
the other way.

Only this site needed it. The other three fan-outs already no-op on empty
without logging: tasks.ts logs only on a real `userRowsError`, golf.ts maps
over an empty array, and player-notify.ts returns early on `!userRows?.length`.

Verified: typecheck 0.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NbDxyygyXRUEERuGocpZZH

* chore(control-plane): clear #1691's expired transitional row, add #1692's, and say why

Two coordination fixes, both from the residue checker reporting honestly.

**#1691's row is removed.** It was TRANSITIONALLY closed — merged at a03ef84
while HEAD sat on that exact commit, which is the narrow exception that stops a
PR carrying its own row from failing main the second it merges. The grace ends
the moment any other commit lands, and it has: this branch added its own. The
row going stale is the exception working exactly as designed, not a defect.

**The failure now names the remedy.** It previously read only "merged at
<sha>, HEAD has moved past it", which a session meeting it cold cannot
distinguish from an ordinary stale row left behind by carelessness. The two
have the same symptom and different histories, and only one of them has a
one-line fix. It now says to delete the row in the PR already being opened.
That is the whole coordination cost of the transitional exception, and it
belongs in the tool's own output rather than in a comment block someone has to
already know to read.

**#1692's row is added** in the same change, which is the pattern the file's own
$comment prescribes: the next ordinary PR clears the expired row and registers
itself at the same time.

Verified: the notice renders — classifyDispositionResidue exercised directly
with a MERGED row whose merge sha differs from HEAD emits the new text. guards
32/32, residue suite 14/14, JSON parses.

Expect one red `open-pr-residue` run between the PR opening and this commit
reaching its head: in-flight arrival is proven by reading the row at the PR's
own head, and the head does not carry it until this lands. That is the
mechanism, not a regression.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NbDxyygyXRUEERuGocpZZH

* docs(incident): correct the Supabase Preview claim — it is inert, not red

I wrote, earlier in this run, that `Supabase Preview` "has been red on every
main commit for so long that it reads as background noise." That is wrong.

Measured across eight PRs (#1679, #1680, #1686#1691), its conclusion is
SKIPPED every time — never FAILURE. It is also not one of main's six required
contexts, so it could not block a merge even if it did fail. A check that never
runs is invisible in a way a red one is not, which makes the real situation
worse than the one I described, not better.

The skipping is legitimate: Supabase only builds a preview branch for a PR that
touches supabase/migrations/**, and most do not. Branching is enabled and works
— #1681 touches migrations and its preview branch reached FUNCTIONS_DEPLOYED
today.

What is genuinely unaddressed is one level down: the Supabase branch record for
`main` carries status MIGRATIONS_FAILED with created_at == updated_at ==
2026-07-03T21:11:11Z, so it has not been refreshed since it was written.
Whether that is a live verdict on today's migrations or a stale setup artifact
is NOT established — both readings fit the timestamps, and picking one would be
a guess. Recorded as unresolved rather than resolved in the wrong direction.

This changes the shape of the "make Supabase Preview green" task: the check is
inert, not failing, so the decision is whether an inert check should be
required, repaired, or removed — not how to turn it green.

One consequence worth having: a preview branch is an isolated project, NOT
production, which makes it the environment the plan's "do not use production
for destructive behavioral tests" rule points toward. With Docker down it is
the only remaining route for exercising the golf-history migration. Not taken
from here — provisioning one is a real resource on the owner's account.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NbDxyygyXRUEERuGocpZZH

* chore(docs): regenerate the document authority inventory

The Supabase Preview correction changed a STATE_SNAPSHOT column and the
inventory went stale again. CI caught it, correctly.

Third time this run, same root cause, worth naming: this generator reads the
git INDEX, and `--check` in CI runs against the COMMIT. Regenerating before the
last content edit produces a locally-green, remotely-red result every time. The
regeneration has to be the final step before the commit, not a step during it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NbDxyygyXRUEERuGocpZZH

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant