The bug
visual_capture_unobtainable_sha is written but never read back. The row→record mapper for a cached pull request does not map the column, so pr.visualCaptureUnobtainableSha is undefined at every reader, and captureUnobtainable is permanently false.
| stage |
file |
state |
| column |
src/db/schema.ts:507 |
present |
| write |
markPullRequestVisualCaptureUnobtainable, src/db/repositories.ts:4739 |
works |
| clear |
src/db/repositories.ts:4699 |
works |
| row → record mapper |
src/db/repositories.ts:7505-7507 |
field is absent |
| read |
src/queue/processors.ts:3542 and :12612 |
always undefined === headSha → false |
Its two siblings written by the very same code path, visualCaptureSatisfiedSha and visualCaptureRetryPendingSha, are both mapped on the adjacent lines. This one was missed.
Why TSC did not catch it
PullRequestRecord.visualCaptureUnobtainableSha is declared optional (src/types.ts:778):
visualCaptureUnobtainableSha?: string | null | undefined;
A required field would have made the mapper a compile error the moment the column was added. Optional makes "never populated" indistinguishable from "populated with undefined", so nothing anywhere fails.
Correction to an earlier draft of this issue: all three visualCapture* fields are optional on this type, not just this one — visualCaptureSatisfiedSha (:774) and visualCaptureRetryPendingSha (:785) are ?: as well. So optionality is not what distinguishes the broken field from its working siblings; the other two were simply mapped and this one was not. That makes the class wider than "one field was declared wrong": every one of these three could have been missed with no compiler signal, and two happened not to be.
Consequence
#9881 exists to stop the screenshot-table gate closing a contributor PR over evidence the bot is structurally unable to produce — a repo with no preview deployment at all. That degrade is gated on captureUnobtainable, so it has never fired. A contributor on such a repo gets the undegraded close, which they cannot reopen.
Two follow-ups were then built on top of the same signal and are inert for the same reason:
Evidence (Orb, 2026-07-31)
rows_with_unobtainable | rows_with_satisfied | total
1 | 8 | 12863
github_app.screenshot_table_close_degraded_capture_unobtainable, last 30 days: 0
One PR row carries the mark, so the write path demonstrably works. Zero degrade events in 30 days, so no reader has ever acted on it.
Fix
- Map the column in the pull-request record mapper, beside its two siblings.
- Make the field required on
PullRequestRecord, matching visualCaptureSatisfiedSha and visualCaptureRetryPendingSha, so the next column added to this write path cannot repeat this silently.
- Regression test: write the mark, read the record back, assert the gate degrades.
test/unit/queue-3.test.ts has the screenshot-table gate (#9881) cases to sit beside.
Worth checking in the same pass
Whether any OTHER optional field on PullRequestRecord is written to the DB but missing from the mapper. The audit is mechanical: every pullRequests column in schema.ts that some .set({...}) writes should appear in the mapper. A scripts/check-*.ts guard over that pairing would close the class rather than this instance — the same shape as check-focus-manifest-fields.ts.
How this was found
While covering the visual-capture branches for #10227: a test asserted visualCaptureUnobtainableSha on the record after driving a real previewUnobtainable capture through the pipeline, and read undefined while the DB column held the value.
The bug
visual_capture_unobtainable_shais written but never read back. The row→record mapper for a cached pull request does not map the column, sopr.visualCaptureUnobtainableShaisundefinedat every reader, andcaptureUnobtainableis permanentlyfalse.src/db/schema.ts:507markPullRequestVisualCaptureUnobtainable,src/db/repositories.ts:4739src/db/repositories.ts:4699src/db/repositories.ts:7505-7507src/queue/processors.ts:3542and:12612undefined === headSha→falseIts two siblings written by the very same code path,
visualCaptureSatisfiedShaandvisualCaptureRetryPendingSha, are both mapped on the adjacent lines. This one was missed.Why TSC did not catch it
PullRequestRecord.visualCaptureUnobtainableShais declared optional (src/types.ts:778):A required field would have made the mapper a compile error the moment the column was added. Optional makes "never populated" indistinguishable from "populated with undefined", so nothing anywhere fails.
Correction to an earlier draft of this issue: all three
visualCapture*fields are optional on this type, not just this one —visualCaptureSatisfiedSha(:774) andvisualCaptureRetryPendingSha(:785) are?:as well. So optionality is not what distinguishes the broken field from its working siblings; the other two were simply mapped and this one was not. That makes the class wider than "one field was declared wrong": every one of these three could have been missed with no compiler signal, and two happened not to be.Consequence
#9881 exists to stop the screenshot-table gate closing a contributor PR over evidence the bot is structurally unable to produce — a repo with no preview deployment at all. That degrade is gated on
captureUnobtainable, so it has never fired. A contributor on such a repo gets the undegradedclose, which they cannot reopen.Two follow-ups were then built on top of the same signal and are inert for the same reason:
Evidence (Orb, 2026-07-31)
One PR row carries the mark, so the write path demonstrably works. Zero degrade events in 30 days, so no reader has ever acted on it.
Fix
PullRequestRecord, matchingvisualCaptureSatisfiedShaandvisualCaptureRetryPendingSha, so the next column added to this write path cannot repeat this silently.test/unit/queue-3.test.tshas thescreenshot-table gate (#9881)cases to sit beside.Worth checking in the same pass
Whether any OTHER optional field on
PullRequestRecordis written to the DB but missing from the mapper. The audit is mechanical: everypullRequestscolumn inschema.tsthat some.set({...})writes should appear in the mapper. Ascripts/check-*.tsguard over that pairing would close the class rather than this instance — the same shape ascheck-focus-manifest-fields.ts.How this was found
While covering the visual-capture branches for #10227: a test asserted
visualCaptureUnobtainableShaon the record after driving a realpreviewUnobtainablecapture through the pipeline, and readundefinedwhile the DB column held the value.