sim: merge PR #17346

This commit is contained in:
Merge Sim
2026-08-30 16:12:55 -07:00
8 changed files with 2161 additions and 0 deletions
+1
View File
@@ -115,6 +115,7 @@ docs/**
!docs/reference/ssh-execution-boundary.md
!docs/reference/ssh-host-key-verification.md
!docs/reference/ssh-reconnect-source-recovery.md
!docs/reference/upstream-breakage-diagnosis.md
!docs/reference/windows-setup-shell.md
!docs/reference/worktree-scan-fingerprint.md
!docs/reference/wsl-command-execution.md
+11
View File
@@ -39,6 +39,17 @@ Never use vague names like `helpers`, `utils`, `common`, `misc`, or `shared-stuf
Always use the primary working directory (the worktree) for all file reads and edits. Never follow absolute paths from subagent results that point to the main repo.
## Red After Merging Main
Before diagnosing a branch — or a whole stack — that went red after merging `main`, check whether `main` was already broken. No workflow runs the test suite on a push to `main`, so its commits carry no checks and the question has to be answered from the PRs whose CI ran against `main` at that moment:
```sh
pnpm run diagnose:upstream-breakage at <commit> --window-hours 1
pnpm run diagnose:upstream-breakage compare <pr> <pr> ...
```
An identical failure set across independent branches is upstream by construction, and the repair is to merge a newer `main` — not to fix the branches. The probe answers `broken` / `clean` / `unknown` and never reports `clean` without positive evidence. See [`docs/reference/upstream-breakage-diagnosis.md`](./docs/reference/upstream-breakage-diagnosis.md).
## Cross-Platform Support
Orca targets macOS, Linux, and Windows. Keep all platform-dependent behavior behind runtime checks:
@@ -0,0 +1,514 @@
{
"_source": "Captured verbatim from GitHub: gh api repos/{owner}/{repo}/commits/<headRefOid>/check-runs",
"prs": {
"16917": {
"pr": {
"baseRefName": "main",
"headRefName": "brennanb2025/mob-create-flow-perf",
"headRefOid": "29be0cc2e032ef03b93ebda9c03809bf9f488c1f",
"mergedAt": "2026-08-28T22:45:54Z",
"number": 16917,
"title": "fix(mobile): create-worktree sheet dies after picking a source and loses the picked PR"
},
"runs": [
{
"appSlug": "github-actions",
"completedAt": "2026-08-28T11:23:30Z",
"conclusion": "skipped",
"name": "test",
"status": "completed"
},
{
"appSlug": "github-actions",
"completedAt": "2026-08-28T11:23:30Z",
"conclusion": "skipped",
"name": "managed hooks on Node 18",
"status": "completed"
},
{
"appSlug": "github-actions",
"completedAt": "2026-08-28T11:23:30Z",
"conclusion": "skipped",
"name": "e2e",
"status": "completed"
},
{
"appSlug": "github-actions",
"completedAt": "2026-08-28T11:23:30Z",
"conclusion": "skipped",
"name": "detect changed e2e specs",
"status": "completed"
},
{
"appSlug": "github-actions",
"completedAt": "2026-08-28T11:23:30Z",
"conclusion": "skipped",
"name": "Git compatibility",
"status": "completed"
},
{
"appSlug": "github-actions",
"completedAt": "2026-08-28T11:23:30Z",
"conclusion": "skipped",
"name": "static analysis",
"status": "completed"
},
{
"appSlug": "github-actions",
"completedAt": "2026-08-28T11:23:30Z",
"conclusion": "skipped",
"name": "package",
"status": "completed"
},
{
"appSlug": "github-actions",
"completedAt": "2026-08-28T11:23:30Z",
"conclusion": "skipped",
"name": "cross-version wire compatibility",
"status": "completed"
},
{
"appSlug": "github-actions",
"completedAt": "2026-08-28T11:23:30Z",
"conclusion": "skipped",
"name": "shell contracts",
"status": "completed"
},
{
"appSlug": "github-actions",
"completedAt": "2026-08-28T11:23:30Z",
"conclusion": "skipped",
"name": "package (windows)",
"status": "completed"
},
{
"appSlug": "github-actions",
"completedAt": "2026-08-28T11:23:30Z",
"conclusion": "skipped",
"name": "orcad browser provider",
"status": "completed"
},
{
"appSlug": "github-actions",
"completedAt": "2026-08-28T11:23:30Z",
"conclusion": "skipped",
"name": "prepare test native cache node 24",
"status": "completed"
},
{
"appSlug": "github-actions",
"completedAt": "2026-08-28T11:23:30Z",
"conclusion": "skipped",
"name": "typecheck",
"status": "completed"
},
{
"appSlug": "github-actions",
"completedAt": "2026-08-28T11:23:36Z",
"conclusion": "success",
"name": "verify",
"status": "completed"
},
{
"appSlug": "github-actions",
"completedAt": "2026-08-28T11:23:30Z",
"conclusion": "skipped",
"name": "xterm patch sync",
"status": "completed"
},
{
"appSlug": "github-actions",
"completedAt": "2026-08-28T11:23:29Z",
"conclusion": "success",
"name": "root directory guard",
"status": "completed"
},
{
"appSlug": "github-actions",
"completedAt": "2026-08-28T11:23:30Z",
"conclusion": "success",
"name": "detect code-relevant changes",
"status": "completed"
},
{
"appSlug": "github-actions",
"completedAt": "2026-08-28T11:23:12Z",
"conclusion": "success",
"name": "test vs non-test LoC",
"status": "completed"
},
{
"appSlug": "github-actions",
"completedAt": "2026-08-28T11:25:42Z",
"conclusion": "success",
"name": "verify",
"status": "completed"
}
]
},
"17002": {
"pr": {
"baseRefName": "main",
"headRefName": "nwparker/headless-linux-server-doc-fixes",
"headRefOid": "3d4f9bb9158cf5aaa6f61f88afc63fa00f0add4a",
"mergedAt": "2026-08-28T09:37:52Z",
"number": 17002,
"title": "docs(headless-server): fix package list, extraction perms, and ldd command"
},
"runs": [
{
"appSlug": "github-actions",
"completedAt": "2026-08-28T09:13:30Z",
"conclusion": "skipped",
"name": "Git compatibility",
"status": "completed"
},
{
"appSlug": "github-actions",
"completedAt": "2026-08-28T09:13:30Z",
"conclusion": "skipped",
"name": "prepare test native cache node 24",
"status": "completed"
},
{
"appSlug": "github-actions",
"completedAt": "2026-08-28T09:13:30Z",
"conclusion": "skipped",
"name": "managed hooks on Node 18",
"status": "completed"
},
{
"appSlug": "github-actions",
"completedAt": "2026-08-28T09:13:30Z",
"conclusion": "skipped",
"name": "xterm patch sync",
"status": "completed"
},
{
"appSlug": "github-actions",
"completedAt": "2026-08-28T09:13:30Z",
"conclusion": "skipped",
"name": "test",
"status": "completed"
},
{
"appSlug": "github-actions",
"completedAt": "2026-08-28T09:13:30Z",
"conclusion": "skipped",
"name": "shell contracts",
"status": "completed"
},
{
"appSlug": "github-actions",
"completedAt": "2026-08-28T09:13:30Z",
"conclusion": "skipped",
"name": "package (windows)",
"status": "completed"
},
{
"appSlug": "github-actions",
"completedAt": "2026-08-28T09:13:30Z",
"conclusion": "skipped",
"name": "e2e",
"status": "completed"
},
{
"appSlug": "github-actions",
"completedAt": "2026-08-28T09:13:30Z",
"conclusion": "skipped",
"name": "package",
"status": "completed"
},
{
"appSlug": "github-actions",
"completedAt": "2026-08-28T09:13:30Z",
"conclusion": "skipped",
"name": "orcad browser provider",
"status": "completed"
},
{
"appSlug": "github-actions",
"completedAt": "2026-08-28T09:13:30Z",
"conclusion": "skipped",
"name": "static analysis",
"status": "completed"
},
{
"appSlug": "github-actions",
"completedAt": "2026-08-28T09:13:30Z",
"conclusion": "skipped",
"name": "detect changed e2e specs",
"status": "completed"
},
{
"appSlug": "github-actions",
"completedAt": "2026-08-28T09:13:30Z",
"conclusion": "skipped",
"name": "typecheck",
"status": "completed"
},
{
"appSlug": "github-actions",
"completedAt": "2026-08-28T09:13:30Z",
"conclusion": "skipped",
"name": "cross-version wire compatibility",
"status": "completed"
},
{
"appSlug": "github-actions",
"completedAt": "2026-08-28T09:13:35Z",
"conclusion": "success",
"name": "verify",
"status": "completed"
},
{
"appSlug": "github-actions",
"completedAt": "2026-08-28T09:13:29Z",
"conclusion": "success",
"name": "detect code-relevant changes",
"status": "completed"
},
{
"appSlug": "github-actions",
"completedAt": "2026-08-28T09:13:29Z",
"conclusion": "success",
"name": "root directory guard",
"status": "completed"
},
{
"appSlug": "github-actions",
"completedAt": "2026-08-28T09:13:13Z",
"conclusion": "success",
"name": "test vs non-test LoC",
"status": "completed"
}
]
},
"17358": {
"pr": {
"baseRefName": "main",
"headRefName": "nwparker/react-purity-lint-cleanup",
"headRefOid": "fb2fde0bcc0290c96b5d5a7005d118dd89de63f5",
"mergedAt": "2026-08-30T08:17:29Z",
"number": 17358,
"title": "Consolidate the renderer now-clock and drop the epoch setState round-trip"
},
"runs": [
{
"appSlug": "github-actions",
"completedAt": "2026-08-30T08:17:08Z",
"conclusion": "success",
"name": "verify",
"status": "completed"
},
{
"appSlug": "github-actions",
"completedAt": "2026-08-30T08:08:11Z",
"conclusion": "skipped",
"name": "e2e",
"status": "completed"
},
{
"appSlug": "github-actions",
"completedAt": "2026-08-30T08:06:30Z",
"conclusion": "skipped",
"name": "prepare test native cache node 24",
"status": "completed"
},
{
"appSlug": "github-actions",
"completedAt": "2026-08-30T08:06:30Z",
"conclusion": "skipped",
"name": "orcad browser provider",
"status": "completed"
},
{
"appSlug": "github-actions",
"completedAt": "2026-08-30T08:06:30Z",
"conclusion": "skipped",
"name": "managed hooks on Node 18",
"status": "completed"
},
{
"appSlug": "github-actions",
"completedAt": "2026-08-30T08:06:30Z",
"conclusion": "skipped",
"name": "cross-version wire compatibility",
"status": "completed"
},
{
"appSlug": "github-actions",
"completedAt": "2026-08-30T08:06:30Z",
"conclusion": "skipped",
"name": "xterm patch sync",
"status": "completed"
},
{
"appSlug": "github-actions",
"completedAt": "2026-08-30T08:06:30Z",
"conclusion": "skipped",
"name": "Git compatibility",
"status": "completed"
},
{
"appSlug": "github-actions",
"completedAt": "2026-08-30T08:06:30Z",
"conclusion": "skipped",
"name": "shell contracts",
"status": "completed"
},
{
"appSlug": "github-actions",
"completedAt": "2026-08-30T08:15:35Z",
"conclusion": "success",
"name": "test / tests node 24 1/8",
"status": "completed"
},
{
"appSlug": "github-actions",
"completedAt": "2026-08-30T08:12:59Z",
"conclusion": "success",
"name": "test / tests node 24 2/8",
"status": "completed"
},
{
"appSlug": "github-actions",
"completedAt": "2026-08-30T08:13:04Z",
"conclusion": "success",
"name": "test / tests node 24 5/8",
"status": "completed"
},
{
"appSlug": "github-actions",
"completedAt": "2026-08-30T08:13:37Z",
"conclusion": "success",
"name": "test / tests node 24 6/8",
"status": "completed"
},
{
"appSlug": "github-actions",
"completedAt": "2026-08-30T08:11:28Z",
"conclusion": "success",
"name": "test / tests node 24 4/8",
"status": "completed"
},
{
"appSlug": "github-actions",
"completedAt": "2026-08-30T08:16:59Z",
"conclusion": "success",
"name": "test / tests node 24 7/8",
"status": "completed"
},
{
"appSlug": "github-actions",
"completedAt": "2026-08-30T08:11:51Z",
"conclusion": "success",
"name": "test / tests node 24 3/8",
"status": "completed"
},
{
"appSlug": "github-actions",
"completedAt": "2026-08-30T08:11:49Z",
"conclusion": "success",
"name": "test / tests node 24 8/8",
"status": "completed"
},
{
"appSlug": "github-actions",
"completedAt": "2026-08-30T08:13:14Z",
"conclusion": "success",
"name": "static analysis",
"status": "completed"
},
{
"appSlug": "github-actions",
"completedAt": "2026-08-30T08:11:58Z",
"conclusion": "success",
"name": "package (windows)",
"status": "completed"
},
{
"appSlug": "github-actions",
"completedAt": "2026-08-30T08:10:58Z",
"conclusion": "success",
"name": "package",
"status": "completed"
},
{
"appSlug": "github-actions",
"completedAt": "2026-08-30T08:08:11Z",
"conclusion": "success",
"name": "detect changed e2e specs",
"status": "completed"
},
{
"appSlug": "github-actions",
"completedAt": "2026-08-30T08:09:16Z",
"conclusion": "success",
"name": "typecheck",
"status": "completed"
},
{
"appSlug": "github-actions",
"completedAt": "2026-08-30T08:07:45Z",
"conclusion": "success",
"name": "root directory guard",
"status": "completed"
},
{
"appSlug": "github-actions",
"completedAt": "2026-08-30T08:06:30Z",
"conclusion": "success",
"name": "detect code-relevant changes",
"status": "completed"
},
{
"appSlug": "greptile-apps",
"completedAt": "2026-08-30T08:07:17Z",
"conclusion": "success",
"name": "Greptile Review",
"status": "completed"
},
{
"appSlug": "github-actions",
"completedAt": "2026-08-30T08:06:39Z",
"conclusion": "success",
"name": "test vs non-test LoC",
"status": "completed"
}
]
}
},
"siblings": [
{
"baseRefName": "brennanb2025/claude-structured-mobile",
"headRefName": "brennanb2025/claude-structured-core-merge",
"number": 15356,
"title": "Merge codex-structured-core into Claude lane"
},
{
"baseRefName": "brennanb2025/claude-structured-mobile",
"headRefName": "brennanb2025/claude-lane-replay-waiter-identity",
"number": 15657,
"title": "fix(native-chat): adopt only the real user replay as a Claude send's identity"
},
{
"baseRefName": "brennanb2025/probe-liveness-coercion-r1",
"headRefName": "brennanb2025/window-close-guard-r1",
"number": 17044,
"title": "fix(terminal): ask before closing the window when a terminal probe could not answer"
},
{
"baseRefName": "brennanb2025/probe-liveness-coercion-r1",
"headRefName": "brennanb2025/pty-absence-verdict-r1",
"number": 17058,
"title": "fix(pty): tell a watched local exit apart from a lost route"
},
{
"baseRefName": "brennanb2025/probe-liveness-coercion-r1",
"headRefName": "brennanb2025/cleanup-post-confirm-liveness-r1",
"number": 17074,
"title": "fix(cleanup): stop deleting a workspace after confirm when it turns out to be busy"
}
]
}
@@ -0,0 +1,519 @@
// Decides whether a set of red branches is red because `main` was already broken
// (upstream) or because the branches themselves broke something.
//
// Why this exists: `main` carries no checks of its own. `pr.yml` — the workflow
// that holds the tests, typecheck and static analysis — is `on: pull_request`
// only, and `unit-tests.yml` is `workflow_call` only, so no commit on `main` has
// a test result attached to it. Asking "was main green at SHA X?" directly
// returns nothing, which is why nobody reaches for it. The answer has to be
// reconstructed from the PRs whose CI ran against `main` at that moment.
//
// See docs/reference/upstream-breakage-diagnosis.md.
// Verdicts. `unknown` is the default: a probe that cannot see enough evidence
// must never report `clean`, because "no evidence of breakage" and "evidence of
// no breakage" are the two states this tool exists to keep apart.
export const VERDICT = {
broken: 'broken',
clean: 'clean',
unknown: 'unknown',
upstream: 'upstream',
sharedAncestor: 'shared-ancestor',
divergent: 'divergent',
noFailures: 'no-failures'
}
// Roll-up jobs reprint another job's failure; counting one as an independent
// failure double-counts the same breakage and makes divergent sets look identical.
export const ROLLUP_CHECKS = new Set(['verify'])
// Reds that are red for reasons unrelated to the commit under test. Counting
// these as breakage makes every window look broken.
export const KNOWN_FALSE_REDS = new Set([
'test / tests node 24 1/8',
'test / tests node 24 6/8',
'e2e / ssh docker watcher isolation'
])
// Third-party review bots post checks on the same commit. They are not signal
// about the tree, so only first-party Actions checks count by default.
export const DEFAULT_APP_SLUGS = new Set(['github-actions'])
// Lanes that actually execute the tree, so a green one is evidence `main` built,
// type-checked or ran. Everything else on a PR — the path classifier, the
// root-directory guard, the LoC counter, the community-PR labeller — stays green
// on a `main` that is entirely broken, so a witness carrying only those witnessed
// nothing.
//
// An allowlist, and deliberately so: an unrecognised name here costs a witness
// and the verdict degrades to `unknown`, whereas an unrecognised name in a
// denylist would count as a full witness and hand back `clean` — the failure this
// list exists to close. `normalizeChecks` reports every name it did not
// recognise so a renamed lane is loud rather than silently dropped.
export const WITNESSING_CHECK_PATTERNS = [
// pr.yml: the lanes that build, type-check, lint or run the suite.
/^typecheck$/,
/^static analysis$/,
/^test$/,
/^test \/ tests\b/,
/^prepare test native cache\b/,
/^package$/,
/^package \(/,
/^managed hooks on Node \d+$/,
/^shell contracts$/,
/^Git compatibility$/,
/^xterm patch sync$/,
/^cross-version wire compatibility$/,
/^orcad browser provider$/,
// e2e.yml: the shards and the app build. `changed e2e specs` only reads the
// diff, so it is not one of these.
/^e2e$/,
/^e2e \d+-of-\d+$/,
/^e2e \/ (?!changed e2e specs$)/,
/^build e2e app$/,
/^prepare Electron native cache$/,
/^ssh docker watcher isolation$/,
// Platform smoke and IME lanes.
/^real IME/,
/^native-smoke \(/,
/^mac-native-owner-smoke$/
]
// Would this check have gone red if `main` were broken?
export function isWitnessingCheck(name, patterns = WITNESSING_CHECK_PATTERNS) {
return patterns.some((pattern) => pattern.test(name))
}
// Branches everything else forks from. Two PRs based on trunk are independent;
// two based on the same feature branch share that branch's diff.
export const TRUNK_REFS = new Set(['main', 'master'])
const FAILING_CONCLUSIONS = new Set(['failure', 'timed_out', 'action_required'])
const NON_RUN_CONCLUSIONS = new Set(['skipped', 'neutral'])
// Minimum independent witnesses before any positive verdict. One witness cannot
// separate "main is broken" from "this branch is broken".
export const MIN_WITNESSES = 2
// Two checks that ran days apart observed two different `main`s, so agreeing
// failures say nothing about either. Beyond this span the answer is `unknown`.
export const MAX_EVIDENCE_SPAN_HOURS = 24
// Splits a commit's check runs into the real failures and everything deliberately
// dropped. `excluded` is returned rather than discarded so callers can print it —
// a silently narrowed population reads as full coverage.
export function normalizeChecks(checkRuns, options = {}) {
const includeKnownFalse = options.includeKnownFalse === true
const appSlugs = options.appSlugs ?? DEFAULT_APP_SLUGS
const witnessPatterns = options.witnessingCheckPatterns ?? WITNESSING_CHECK_PATTERNS
const failures = []
const ran = []
const excluded = { rollup: [], knownFalse: [], foreignApp: [], nonWitnessing: [] }
let incomplete = 0
let completedAt = null
for (const run of checkRuns) {
const name = run.name
const slug = run.app?.slug ?? run.appSlug ?? null
if (slug !== null && !appSlugs.has(slug)) {
if (FAILING_CONCLUSIONS.has(run.conclusion)) {
excluded.foreignApp.push(name)
}
continue
}
if (ROLLUP_CHECKS.has(name)) {
if (FAILING_CONCLUSIONS.has(run.conclusion)) {
excluded.rollup.push(name)
}
continue
}
if (run.status !== 'completed') {
incomplete += 1
continue
}
if (NON_RUN_CONCLUSIONS.has(run.conclusion)) {
continue
}
if (!includeKnownFalse && KNOWN_FALSE_REDS.has(name)) {
if (FAILING_CONCLUSIONS.has(run.conclusion)) {
excluded.knownFalse.push(name)
}
continue
}
ran.push(name)
if (
run.completedAt !== null &&
run.completedAt !== undefined &&
run.completedAt > (completedAt ?? '')
) {
completedAt = run.completedAt
}
if (FAILING_CONCLUSIONS.has(run.conclusion)) {
failures.push(name)
}
}
// Dedupe: a re-run posts the same check name twice, and a repeated name would
// make two identical failure sets compare as different multisets.
const uniqueRan = [...new Set(ran)].sort()
const witnessing = uniqueRan.filter((name) => isWitnessingCheck(name, witnessPatterns))
excluded.nonWitnessing = uniqueRan.filter((name) => !isWitnessingCheck(name, witnessPatterns))
return {
failures: [...new Set(failures)].sort(),
ran: uniqueRan,
// The subset of `ran` that would have gone red had `main` been broken.
witnessing,
excluded,
incomplete,
completedAt,
// Only a PR on which some lane actually executed the tree witnessed anything.
// A path-filtered PR still posts its path classifier, root-directory guard and
// LoC counter green; counting those as a witness is how `clean` gets returned
// for a window nothing was tested in.
usable: incomplete === 0 && witnessing.length > 0
}
}
// Groups PRs into stacks by branch parentage: a PR whose base branch is another
// PR's head branch is stacked on it. Two refs in the same stack share a diff, so
// they are one witness, not two.
export function buildStacks(prs, options = {}) {
const trunkRefs = options.trunkRefs ?? TRUNK_REFS
const headToNumber = new Map()
for (const pr of prs) {
headToNumber.set(pr.headRefName, pr.number)
}
const parent = new Map(prs.map((pr) => [pr.number, pr.number]))
const find = (n) => {
let root = n
while (parent.get(root) !== root) {
root = parent.get(root)
}
return root
}
const union = (a, b) => {
const left = find(a)
const right = find(b)
if (left !== right) {
parent.set(left, right)
}
}
// Siblings on one non-trunk base share that base's diff whether or not the base
// itself is in this list. Grouping only by listed parents counts two children of
// an unlisted parent as two independent stacks, so the corroboration the
// `broken` and `upstream` verdicts rest on is one witness counted twice.
const firstOnBase = new Map()
for (const pr of prs) {
const baseRef = pr.baseRefName
if (baseRef === null || baseRef === undefined || trunkRefs.has(baseRef)) {
continue
}
if (baseRef === pr.headRefName) {
continue
}
const listedParent = headToNumber.get(baseRef)
if (listedParent !== undefined && listedParent !== pr.number) {
union(pr.number, listedParent)
}
const sibling = firstOnBase.get(baseRef)
if (sibling === undefined) {
firstOnBase.set(baseRef, pr.number)
} else {
union(pr.number, sibling)
}
}
const groups = new Map()
for (const pr of prs) {
const root = find(pr.number)
if (!groups.has(root)) {
groups.set(root, [])
}
groups.get(root).push(pr.number)
}
return [...groups.values()].map((members) => members.sort((a, b) => a - b))
}
// Wall-clock spread between the earliest and latest witness. Returns null when
// fewer than two witnesses carry a timestamp, so callers cannot read a missing
// span as a tight one.
export function evidenceSpanHours(witnesses) {
const stamps = witnesses
.map((w) => w.completedAt)
.filter((s) => s !== null && s !== undefined)
.map((s) => new Date(s).getTime())
.filter((t) => Number.isFinite(t))
if (stamps.length < 2) {
return null
}
return (Math.max(...stamps) - Math.min(...stamps)) / 3600000
}
// How a single check behaved across the witnesses.
export const CHECK_KIND = {
// Red in every witness that ran it: main was broken for the whole window.
alwaysRed: 'always-red',
// Every red falls inside one unbroken stretch of time with no green in it:
// main broke at the start of that stretch. Covers a break that is still open
// (no greens after), one that was fixed (greens after), and one that both
// opened and closed inside the window.
windowed: 'windowed',
// Greens fall inside the red stretch: the branches differ, not main.
interleaved: 'interleaved',
// Too few witnesses or stacks to attribute either way.
tooNarrow: 'too-narrow'
}
function timeOf(w) {
const t =
w.completedAt === null || w.completedAt === undefined
? Number.NaN
: new Date(w.completedAt).getTime()
return Number.isFinite(t) ? t : null
}
// Classifies one check name by whether its reds and greens separate cleanly in
// time. A clean split is a main-side transition; interleaving is branch-specific.
function classifyOneCheck(name, usable, minWitnesses) {
const red = usable.filter((w) => w.failures.includes(name))
const green = usable.filter((w) => !w.failures.includes(name) && w.ran.includes(name))
const redStacks = new Set(red.map((w) => w.stackId ?? w.ref))
const redTimes = red.map(timeOf).filter((t) => t !== null)
const greenTimes = green.map(timeOf).filter((t) => t !== null)
const base = {
name,
redRefs: red.map((w) => w.ref),
greenRefs: green.map((w) => w.ref),
redStacks: redStacks.size,
firstRed: redTimes.length > 0 ? Math.min(...redTimes) : null,
lastRed: redTimes.length > 0 ? Math.max(...redTimes) : null,
firstGreen: greenTimes.length > 0 ? Math.min(...greenTimes) : null,
lastGreen: greenTimes.length > 0 ? Math.max(...greenTimes) : null
}
// A green inside the red stretch means the tree was fine there, so the reds
// around it belong to their own branches. This is checked before the witness
// thresholds: one red against many greens is branch-specific evidence, not
// thin evidence, and calling it `too-narrow` would make `clean` unreachable.
const greenInsideRedStretch =
base.firstRed !== null && greenTimes.some((t) => t >= base.firstRed && t <= base.lastRed)
if (greenInsideRedStretch) {
return { ...base, kind: CHECK_KIND.interleaved }
}
if (base.firstRed === null) {
return { ...base, kind: CHECK_KIND.tooNarrow, reason: 'no red carries a timestamp' }
}
// A red confined to one stack with greens on both sides of it: main would have
// had to break and be fixed between two adjacent witnesses, which no other
// branch could have inherited. That makes it the branch's own failure.
const bracketedByGreens =
greenTimes.some((t) => t < base.firstRed) && greenTimes.some((t) => t > base.lastRed)
if (redStacks.size < minWitnesses && bracketedByGreens) {
return { ...base, kind: CHECK_KIND.interleaved }
}
if (red.length < minWitnesses || redStacks.size < minWitnesses) {
return {
...base,
kind: CHECK_KIND.tooNarrow,
reason:
redStacks.size < minWitnesses
? `red in only ${redStacks.size} independent stack(s)`
: `red in only ${red.length} witness(es)`
}
}
if (green.length === 0) {
return { ...base, kind: CHECK_KIND.alwaysRed }
}
return {
...base,
kind: CHECK_KIND.windowed,
lastGreenBefore: Math.max(
...greenTimes.filter((t) => t < base.firstRed),
Number.NEGATIVE_INFINITY
),
firstGreenAfter: Math.min(
...greenTimes.filter((t) => t > base.lastRed),
Number.POSITIVE_INFINITY
)
}
}
// True when `main` is known broken for this check at time `at` (ms). The blind
// gaps on either side of the red stretch answer false, not true.
export function brokenAt(check, at) {
if (check.kind === CHECK_KIND.alwaysRed) {
return true
}
if (check.kind === CHECK_KIND.windowed) {
return at >= check.firstRed && at <= check.lastRed
}
return false
}
// Per-check attribution across witnesses.
export function classifyChecks(witnesses, options = {}) {
const minWitnesses = options.minWitnesses ?? MIN_WITNESSES
const usable = witnesses.filter((w) => w.usable)
const names = new Set()
for (const w of usable) {
for (const name of w.failures) {
names.add(name)
}
}
const checks = [...names].sort().map((name) => classifyOneCheck(name, usable, minWitnesses))
const upstream = checks.filter((c) => c.kind === CHECK_KIND.alwaysRed)
const transitions = checks.filter((c) => c.kind === CHECK_KIND.windowed)
const branchSpecific = checks.filter((c) => c.kind === CHECK_KIND.interleaved)
const inconclusive = checks.filter((c) => c.kind === CHECK_KIND.tooNarrow)
const failing = usable.filter((w) => w.failures.length > 0)
const signatures = new Set(failing.map((w) => w.failures.join(' ')))
const failingStacks = new Set(failing.map((w) => w.stackId ?? w.ref))
return {
checks,
upstream,
transitions,
branchSpecific,
inconclusive,
evidenceSpanHours: evidenceSpanHours(usable),
identicalAcrossFailing: failing.length >= minWitnesses && signatures.size === 1,
failingWitnesses: failing.length,
failingStacks: failingStacks.size,
usableWitnesses: usable.length,
totalWitnesses: witnesses.length,
independentStacks: new Set(usable.map((w) => w.stackId ?? w.ref)).size
}
}
// Rejects evidence whose checks are too far apart in time to have observed the
// same `main`. Returns null when the span is acceptable.
function staleEvidenceVerdict(classification, options) {
const maxSpan = options.maxSpanHours ?? MAX_EVIDENCE_SPAN_HOURS
const span = classification.evidenceSpanHours
if (span === null || span === undefined) {
return { verdict: VERDICT.unknown, why: 'fewer than two witnesses carry a check timestamp' }
}
if (span > maxSpan) {
return {
verdict: VERDICT.unknown,
why: `witness checks span ${span.toFixed(1)}h (max ${maxSpan}h); they ran against different mains, so agreeing failures prove nothing`
}
}
return null
}
// `compare` verdict: are these branches red for the same reason or different ones?
export function compareVerdict(classification, options = {}) {
const minWitnesses = options.minWitnesses ?? MIN_WITNESSES
if (classification.usableWitnesses < minWitnesses) {
return {
verdict: VERDICT.unknown,
why: `only ${classification.usableWitnesses} of ${classification.totalWitnesses} refs ran a lane that exercises the tree (need ${minWitnesses})`
}
}
if (classification.failingWitnesses === 0) {
return { verdict: VERDICT.noFailures, why: 'no real failures on any ref' }
}
const stale = staleEvidenceVerdict(classification, options)
if (stale !== null) {
return stale
}
if (classification.failingWitnesses < minWitnesses) {
return {
verdict: VERDICT.unknown,
why: `only ${classification.failingWitnesses} ref is failing; a single red ref cannot be told apart from upstream breakage`
}
}
if (!classification.identicalAcrossFailing) {
return {
verdict: VERDICT.divergent,
why: 'failing refs do not share one failure set, so at least part of the damage is branch-specific'
}
}
if (classification.failingStacks < minWitnesses) {
return {
verdict: VERDICT.sharedAncestor,
why: 'identical failures, but every failing ref is in one stack: the cause is at or below that stack root (main, or the root PR itself)'
}
}
return {
verdict: VERDICT.upstream,
why: 'identical failure set across independent stacks; damage from a merge varies with what each branch changed, so this is upstream'
}
}
// A commit sitting between the last green and the first red of a break — or
// between the last red and the green that follows — is in the blind gap: no
// witness observed main there, so the answer is unknown, not clean.
function inBlindGap(check, at) {
if (check.kind !== CHECK_KIND.windowed) {
return false
}
const beforeGap = at > check.lastGreenBefore && at < check.firstRed
const afterGap = at > check.lastRed && at < check.firstGreenAfter
return beforeGap || afterGap
}
// `at` verdict: was main broken at this commit? Never returns `clean` without
// positive evidence from enough independent witnesses.
export function mainHealthVerdict(classification, at, options = {}) {
const minWitnesses = options.minWitnesses ?? MIN_WITNESSES
if (classification.usableWitnesses < minWitnesses) {
return {
verdict: VERDICT.unknown,
why: `only ${classification.usableWitnesses} of ${classification.totalWitnesses} witness PRs ran a lane that exercises the tree (need ${minWitnesses})`,
brokenChecks: []
}
}
if (classification.independentStacks < minWitnesses) {
return {
verdict: VERDICT.unknown,
why: `witnesses span only ${classification.independentStacks} independent stack(s); they share a diff and cannot corroborate each other`,
brokenChecks: []
}
}
const stale = staleEvidenceVerdict(classification, options)
if (stale !== null) {
return { ...stale, brokenChecks: [] }
}
const broken = classification.checks.filter((c) => brokenAt(c, at))
if (broken.length > 0) {
return {
verdict: VERDICT.broken,
why: `${broken.length} check(s) were failing on main at this commit: ${broken.map((c) => c.name).join(', ')}`,
brokenChecks: broken.map((c) => c.name)
}
}
const gaps = classification.checks.filter((c) => inBlindGap(c, at))
if (gaps.length > 0) {
return {
verdict: VERDICT.unknown,
why: `${gaps.length} check(s) changed state inside this window with no witness at the commit itself: ${gaps.map((c) => c.name).join(', ')}`,
brokenChecks: []
}
}
if (classification.inconclusive.length > 0) {
return {
verdict: VERDICT.unknown,
why: `${classification.inconclusive.length} failing check(s) were seen too narrowly to attribute; treat main as unproven`,
brokenChecks: []
}
}
return {
verdict: VERDICT.clean,
why: `${classification.usableWitnesses} witnesses across ${classification.independentStacks} independent stacks; every check that failed here failed only on its own branch`,
brokenChecks: []
}
}
// Keeps the PRs whose CI actually completed inside the window. A PR merged in
// the window may have last run CI days earlier, against a different `main`.
export function selectWitnessesInWindow(witnesses, from, to) {
return witnesses.filter((w) => {
if (w.completedAt === null || w.completedAt === undefined) {
return false
}
const at = new Date(w.completedAt).getTime()
return at >= from.getTime() && at <= to.getTime()
})
}
@@ -0,0 +1,586 @@
import { readFileSync } from 'node:fs'
import { describe, expect, it } from 'vitest'
import {
CHECK_KIND,
MIN_WITNESSES,
VERDICT,
brokenAt,
buildStacks,
classifyChecks,
compareVerdict,
evidenceSpanHours,
isWitnessingCheck,
mainHealthVerdict,
normalizeChecks,
selectWitnessesInWindow
} from './upstream-breakage-evidence.mjs'
// Verbatim GitHub check-run payloads, not hand-written shapes: the bug these pin
// is that real path-filtered PRs post three green meta jobs and nothing else.
const FIXTURE = JSON.parse(
readFileSync(new URL('./upstream-breakage-check-runs.fixture.json', import.meta.url), 'utf8')
)
const DOCS_ONLY = FIXTURE.prs['17002']
const MOBILE_ONLY = FIXTURE.prs['16917']
const FULL_CI = FIXTURE.prs['17358']
const HOUR = 3600 * 1000
const T0 = Date.parse('2026-08-30T03:00:00Z')
function check(name, conclusion, extra = {}) {
return {
name,
status: 'completed',
conclusion,
appSlug: 'github-actions',
completedAt: new Date(T0).toISOString(),
...extra
}
}
// Witnesses built from the captured payloads, stacked the way the probe stacks
// them, so the verdict path under test is the one the CLI runs.
function realWitnesses(entries) {
const stacks = buildStacks(entries.map((entry) => entry.pr))
const stackOf = new Map()
for (const [index, members] of stacks.entries()) {
for (const number of members) {
stackOf.set(number, `stack-${index + 1}`)
}
}
return entries.map((entry) => ({
ref: `#${entry.pr.number}`,
stackId: stackOf.get(entry.pr.number),
...normalizeChecks(entry.runs)
}))
}
// A usable witness with the given failures, all other named checks green.
function witness(ref, stackId, offsetMinutes, failures, ran = []) {
const names = [...new Set([...failures, ...ran])]
return {
ref,
stackId,
usable: true,
incomplete: 0,
completedAt: new Date(T0 + offsetMinutes * 60 * 1000).toISOString(),
failures: [...failures].sort(),
ran: names.sort(),
excluded: { rollup: [], knownFalse: [], foreignApp: [] }
}
}
describe('normalizeChecks', () => {
it('keeps a real failure and the checks that ran green', () => {
const result = normalizeChecks([
check('typecheck', 'failure'),
check('static analysis', 'success')
])
expect(result.failures).toEqual(['typecheck'])
expect(result.ran).toEqual(['static analysis', 'typecheck'])
expect(result.usable).toBe(true)
})
it('never counts the verify roll-up as an independent failure', () => {
const result = normalizeChecks([check('verify', 'failure'), check('typecheck', 'failure')])
expect(result.failures).toEqual(['typecheck'])
expect(result.excluded.rollup).toEqual(['verify'])
})
it('does not report a green roll-up as an excluded red', () => {
expect(
normalizeChecks([check('verify', 'success'), check('typecheck', 'success')]).excluded.rollup
).toEqual([])
})
it('drops the known-false reds but reports what it dropped', () => {
const result = normalizeChecks([
check('test / tests node 24 1/8', 'failure'),
check('test / tests node 24 6/8', 'failure'),
check('e2e / ssh docker watcher isolation', 'failure'),
check('test / tests node 24 2/8', 'failure')
])
expect(result.failures).toEqual(['test / tests node 24 2/8'])
expect(result.excluded.knownFalse).toHaveLength(3)
})
it('counts the known-false reds when explicitly asked to', () => {
const result = normalizeChecks([check('test / tests node 24 1/8', 'failure')], {
includeKnownFalse: true
})
expect(result.failures).toEqual(['test / tests node 24 1/8'])
})
it('ignores third-party app checks', () => {
const result = normalizeChecks([
check('Greptile Review', 'failure', { appSlug: 'greptile-apps' }),
check('typecheck', 'success')
])
expect(result.failures).toEqual([])
expect(result.excluded.foreignApp).toEqual(['Greptile Review'])
})
it('treats a skipped check as not run rather than as a pass', () => {
const result = normalizeChecks([check('package', 'skipped')])
expect(result.ran).toEqual([])
expect(result.usable).toBe(false)
})
it('is unusable while a check is still in progress', () => {
const result = normalizeChecks([
check('typecheck', null, { status: 'in_progress' }),
check('static analysis', 'success')
])
expect(result.usable).toBe(false)
expect(result.incomplete).toBe(1)
})
it('counts a timed-out check as a failure', () => {
expect(normalizeChecks([check('package', 'timed_out')]).failures).toEqual(['package'])
})
it('dedupes a check name a re-run posted twice', () => {
const result = normalizeChecks([check('typecheck', 'failure'), check('typecheck', 'failure')])
expect(result.failures).toEqual(['typecheck'])
})
// The bug: `#17002` is a docs-only PR and `#16917` a mobile-only one. Both
// path-filter every lane in pr.yml away, and both still post three green
// meta jobs — the path classifier, the root-directory guard and the LoC
// counter. Counting those as a witness is how the probe reported `clean` for a
// window in which no test, typecheck or static-analysis lane had run at all.
it('is not a witness when only the always-on meta jobs ran (docs-only PR #17002)', () => {
const result = normalizeChecks(DOCS_ONLY.runs)
expect(result.incomplete).toBe(0)
expect(result.ran).toEqual([
'detect code-relevant changes',
'root directory guard',
'test vs non-test LoC'
])
expect(result.witnessing).toEqual([])
expect(result.usable).toBe(false)
})
it('is not a witness when only the always-on meta jobs ran (mobile-only PR #16917)', () => {
const result = normalizeChecks(MOBILE_ONLY.runs)
expect(result.witnessing).toEqual([])
expect(result.usable).toBe(false)
})
it('names the checks it refused to treat as witnesses rather than dropping them', () => {
expect(normalizeChecks(DOCS_ONLY.runs).excluded.nonWitnessing).toEqual([
'detect code-relevant changes',
'root directory guard',
'test vs non-test LoC'
])
})
// Positive control for the two above: the same gate on a PR whose lanes really
// ran leaves it usable, so `usable: false` is the meta jobs, not the gate.
it('is a witness when the real lanes ran (full-CI PR #17358)', () => {
const result = normalizeChecks(FULL_CI.runs)
expect(result.usable).toBe(true)
expect(result.witnessing).toContain('typecheck')
expect(result.witnessing).toContain('static analysis')
expect(result.witnessing).toContain('test / tests node 24 2/8')
})
it('reports the latest completion time it saw', () => {
const later = new Date(T0 + HOUR).toISOString()
const result = normalizeChecks([
check('typecheck', 'success'),
check('static analysis', 'success', { completedAt: later })
])
expect(result.completedAt).toBe(later)
})
})
describe('isWitnessingCheck', () => {
it('counts the lanes that build, type-check or run the suite', () => {
for (const name of [
'typecheck',
'static analysis',
'test',
'test / tests node 24 2/8',
'package',
'package (windows)',
'e2e',
'e2e / e2e 3-of-14',
'shell contracts',
'Git compatibility',
'cross-version wire compatibility',
'managed hooks on Node 18'
]) {
expect(isWitnessingCheck(name)).toBe(true)
}
})
it('does not count a job that stays green on a broken main', () => {
for (const name of [
'detect code-relevant changes',
'root directory guard',
'test vs non-test LoC',
'track-community-pr',
'detect changed e2e specs',
'e2e / changed e2e specs'
]) {
expect(isWitnessingCheck(name)).toBe(false)
}
})
// An unrecognised name costs a witness (verdict degrades to `unknown`) instead
// of buying one, which is the whole reason this is an allowlist.
it('does not count a name it has never seen', () => {
expect(isWitnessingCheck('some future bot check')).toBe(false)
})
})
describe('buildStacks', () => {
it('puts unrelated PRs based on main in their own stacks', () => {
const stacks = buildStacks([
{ number: 1, headRefName: 'a', baseRefName: 'main' },
{ number: 2, headRefName: 'b', baseRefName: 'main' }
])
expect(stacks).toHaveLength(2)
})
it('groups a chain of stacked PRs into one stack', () => {
const stacks = buildStacks([
{ number: 1, headRefName: 'a', baseRefName: 'main' },
{ number: 2, headRefName: 'b', baseRefName: 'a' },
{ number: 3, headRefName: 'c', baseRefName: 'b' }
])
expect(stacks).toEqual([[1, 2, 3]])
})
it('groups a chain listed leaf-first', () => {
const stacks = buildStacks([
{ number: 3, headRefName: 'c', baseRefName: 'b' },
{ number: 2, headRefName: 'b', baseRefName: 'a' },
{ number: 1, headRefName: 'a', baseRefName: 'main' }
])
expect(stacks).toEqual([[1, 2, 3]])
})
// The bug: grouping only by *listed* parents counted these as two independent
// stacks, so the corroboration `broken` and `upstream` rest on was one witness
// counted twice. Both are real open PRs on a base branch that has no PR here.
it('counts two PRs on one unlisted parent branch as a single stack', () => {
const siblings = FIXTURE.siblings.filter(
(pr) => pr.baseRefName === 'brennanb2025/claude-structured-mobile'
)
expect(siblings.map((pr) => pr.number).sort((a, b) => a - b)).toEqual([15356, 15657])
expect(siblings.some((pr) => pr.headRefName === siblings[0].baseRefName)).toBe(false)
expect(buildStacks(siblings)).toEqual([[15356, 15657]])
})
it('counts three PRs on one unlisted parent branch as a single stack', () => {
const siblings = FIXTURE.siblings.filter(
(pr) => pr.baseRefName === 'brennanb2025/probe-liveness-coercion-r1'
)
expect(buildStacks(siblings)).toEqual([[17044, 17058, 17074]])
})
it('still separates two PRs on different unlisted parent branches', () => {
const stacks = buildStacks([
{ number: 1, headRefName: 'a', baseRefName: 'parent-one' },
{ number: 2, headRefName: 'b', baseRefName: 'parent-two' }
])
expect(stacks).toEqual([[1], [2]])
})
it('keeps two separate chains separate', () => {
const stacks = buildStacks([
{ number: 1, headRefName: 'a', baseRefName: 'main' },
{ number: 2, headRefName: 'b', baseRefName: 'a' },
{ number: 10, headRefName: 'x', baseRefName: 'main' },
{ number: 11, headRefName: 'y', baseRefName: 'x' }
])
expect(stacks).toEqual([
[1, 2],
[10, 11]
])
})
})
describe('evidenceSpanHours', () => {
it('returns null when fewer than two witnesses carry a timestamp', () => {
expect(evidenceSpanHours([witness('#1', 's1', 0, [])])).toBeNull()
expect(evidenceSpanHours([{ ref: '#1' }, { ref: '#2' }])).toBeNull()
})
it('measures the spread between the earliest and latest witness', () => {
const span = evidenceSpanHours([witness('#1', 's1', 0, []), witness('#2', 's2', 120, [])])
expect(span).toBeCloseTo(2)
})
})
describe('classifyChecks', () => {
it('calls a check red in every witness that ran it always-red', () => {
const result = classifyChecks([
witness('#1', 's1', 0, ['typecheck']),
witness('#2', 's2', 10, ['typecheck'])
])
expect(result.upstream.map((c) => c.name)).toEqual(['typecheck'])
})
it('finds a break that opens partway through the window', () => {
const result = classifyChecks([
witness('#1', 's1', 0, [], ['typecheck']),
witness('#2', 's2', 10, ['typecheck']),
witness('#3', 's3', 20, ['typecheck'])
])
expect(result.transitions).toHaveLength(1)
expect(result.transitions[0].kind).toBe(CHECK_KIND.windowed)
expect(result.transitions[0].lastGreenBefore).toBe(T0)
})
it('finds a break that opened and closed inside the window', () => {
const result = classifyChecks([
witness('#1', 's1', 0, [], ['typecheck']),
witness('#2', 's2', 10, ['typecheck']),
witness('#3', 's3', 20, ['typecheck']),
witness('#4', 's4', 30, [], ['typecheck'])
])
expect(result.transitions).toHaveLength(1)
expect(result.transitions[0].firstGreenAfter).toBe(T0 + 30 * 60 * 1000)
})
it('calls interleaved reds and greens branch-specific', () => {
const result = classifyChecks([
witness('#1', 's1', 0, ['typecheck']),
witness('#2', 's2', 10, [], ['typecheck']),
witness('#3', 's3', 20, ['typecheck'])
])
expect(result.branchSpecific.map((c) => c.name)).toEqual(['typecheck'])
expect(result.transitions).toEqual([])
})
it('calls a lone red bracketed by greens branch-specific, not thin evidence', () => {
const result = classifyChecks([
witness('#1', 's1', 0, [], ['root directory guard']),
witness('#2', 's2', 10, ['root directory guard']),
witness('#3', 's3', 20, [], ['root directory guard'])
])
expect(result.branchSpecific.map((c) => c.name)).toEqual(['root directory guard'])
expect(result.inconclusive).toEqual([])
})
it('refuses to attribute a red confined to one stack', () => {
const result = classifyChecks([
witness('#1', 's1', 0, ['typecheck']),
witness('#2', 's1', 10, ['typecheck'])
])
expect(result.upstream).toEqual([])
expect(result.inconclusive.map((c) => c.name)).toEqual(['typecheck'])
})
it('ignores witnesses whose checks never completed', () => {
const stalled = { ...witness('#3', 's3', 5, ['typecheck']), usable: false }
const result = classifyChecks([witness('#1', 's1', 0, ['typecheck']), stalled])
expect(result.usableWitnesses).toBe(1)
})
it('reports identical failure sets across the failing witnesses', () => {
const result = classifyChecks([
witness('#1', 's1', 0, ['typecheck', 'static analysis']),
witness('#2', 's2', 10, ['static analysis', 'typecheck'])
])
expect(result.identicalAcrossFailing).toBe(true)
})
it('reports divergent failure sets as not identical', () => {
const result = classifyChecks([
witness('#1', 's1', 0, ['typecheck']),
witness('#2', 's2', 10, ['package'])
])
expect(result.identicalAcrossFailing).toBe(false)
})
})
describe('brokenAt', () => {
const windowed = classifyChecks([
witness('#1', 's1', 0, [], ['typecheck']),
witness('#2', 's2', 10, ['typecheck']),
witness('#3', 's3', 20, ['typecheck']),
witness('#4', 's4', 30, [], ['typecheck'])
]).transitions[0]
it('is broken inside the red stretch', () => {
expect(brokenAt(windowed, T0 + 15 * 60 * 1000)).toBe(true)
})
it('is not broken before the stretch opens', () => {
expect(brokenAt(windowed, T0)).toBe(false)
})
it('is not broken after the stretch closes', () => {
expect(brokenAt(windowed, T0 + 30 * 60 * 1000)).toBe(false)
})
it('is broken everywhere for an always-red check', () => {
const always = classifyChecks([
witness('#1', 's1', 0, ['typecheck']),
witness('#2', 's2', 10, ['typecheck'])
]).upstream[0]
expect(brokenAt(always, T0 - HOUR)).toBe(true)
})
it('is never broken for an interleaved check', () => {
const interleaved = classifyChecks([
witness('#1', 's1', 0, ['typecheck']),
witness('#2', 's2', 10, [], ['typecheck']),
witness('#3', 's3', 20, ['typecheck'])
]).branchSpecific[0]
expect(brokenAt(interleaved, T0 + 5 * 60 * 1000)).toBe(false)
})
})
describe('mainHealthVerdict', () => {
it('reports broken and names the checks', () => {
const classification = classifyChecks([
witness('#1', 's1', 0, [], ['typecheck']),
witness('#2', 's2', 10, ['typecheck']),
witness('#3', 's3', 20, ['typecheck'])
])
const verdict = mainHealthVerdict(classification, T0 + 15 * 60 * 1000)
expect(verdict.verdict).toBe(VERDICT.broken)
expect(verdict.brokenChecks).toEqual(['typecheck'])
})
it('answers unknown, never clean, with a single witness', () => {
const verdict = mainHealthVerdict(classifyChecks([witness('#1', 's1', 0, [])]), T0)
expect(verdict.verdict).toBe(VERDICT.unknown)
})
it('answers unknown when every witness is in one stack', () => {
const classification = classifyChecks([witness('#1', 's1', 0, []), witness('#2', 's1', 10, [])])
expect(mainHealthVerdict(classification, T0).verdict).toBe(VERDICT.unknown)
})
it('answers unknown when the witnesses observed different mains', () => {
const classification = classifyChecks([
witness('#1', 's1', 0, []),
witness('#2', 's2', 60 * 48, [])
])
const verdict = mainHealthVerdict(classification, T0)
expect(verdict.verdict).toBe(VERDICT.unknown)
expect(verdict.why).toContain('different mains')
})
it('answers unknown in the blind gap between the last green and the first red', () => {
const classification = classifyChecks([
witness('#1', 's1', 0, [], ['typecheck']),
witness('#2', 's2', 20, ['typecheck']),
witness('#3', 's3', 30, ['typecheck'])
])
const verdict = mainHealthVerdict(classification, T0 + 10 * 60 * 1000)
expect(verdict.verdict).toBe(VERDICT.unknown)
expect(verdict.why).toContain('no witness at the commit itself')
})
it('answers unknown when a failing check was seen too narrowly', () => {
const classification = classifyChecks([
witness('#1', 's1', 0, ['package']),
witness('#2', 's2', 10, []),
witness('#3', 's3', 20, [])
])
expect(mainHealthVerdict(classification, T0 + 5 * 60 * 1000).verdict).toBe(VERDICT.unknown)
})
it('reports clean only with corroborating independent witnesses', () => {
const classification = classifyChecks([
witness('#1', 's1', 0, [], ['typecheck']),
witness('#2', 's2', 10, [], ['typecheck'])
])
expect(mainHealthVerdict(classification, T0 + 5 * 60 * 1000).verdict).toBe(VERDICT.clean)
})
// End-to-end on the payload that reproduced this: two real merged PRs, 2.2h
// apart, in two independent stacks, on each of which nothing but the three
// always-on meta jobs ran. Before the witnessing-lane gate this returned
// `clean` — a green verdict for a window in which nothing was tested.
it('answers unknown, not clean, when no witness ran a lane that exercises the tree', () => {
const witnesses = realWitnesses([DOCS_ONLY, MOBILE_ONLY])
const classification = classifyChecks(witnesses)
expect(classification.independentStacks).toBe(0)
const verdict = mainHealthVerdict(classification, Date.parse(DOCS_ONLY.runs[0].completedAt))
expect(verdict.verdict).not.toBe(VERDICT.clean)
expect(verdict.verdict).toBe(VERDICT.unknown)
expect(verdict.why).toContain('exercises the tree')
})
it('needs at least the documented number of witnesses', () => {
expect(MIN_WITNESSES).toBe(2)
})
})
describe('compareVerdict', () => {
it('calls an identical failure set across independent stacks upstream', () => {
const classification = classifyChecks([
witness('#1', 's1', 0, ['typecheck']),
witness('#2', 's2', 10, ['typecheck'])
])
expect(compareVerdict(classification).verdict).toBe(VERDICT.upstream)
})
it('calls differing failure sets divergent', () => {
const classification = classifyChecks([
witness('#1', 's1', 0, ['typecheck']),
witness('#2', 's2', 10, ['package'])
])
expect(compareVerdict(classification).verdict).toBe(VERDICT.divergent)
})
it('will not call one stack upstream on its own', () => {
const classification = classifyChecks([
witness('#1', 's1', 0, ['typecheck']),
witness('#2', 's1', 10, ['typecheck'])
])
expect(compareVerdict(classification).verdict).toBe(VERDICT.sharedAncestor)
})
it('refuses identical failures whose checks ran days apart', () => {
const classification = classifyChecks([
witness('#1', 's1', 0, ['typecheck']),
witness('#2', 's2', 60 * 71, ['typecheck'])
])
const verdict = compareVerdict(classification)
expect(verdict.verdict).toBe(VERDICT.unknown)
expect(verdict.why).toContain('different mains')
})
it('reports no-failures when nothing is red', () => {
const classification = classifyChecks([
witness('#1', 's1', 0, [], ['typecheck']),
witness('#2', 's2', 10, [], ['typecheck'])
])
expect(compareVerdict(classification).verdict).toBe(VERDICT.noFailures)
})
it('will not judge a single red ref', () => {
const classification = classifyChecks([
witness('#1', 's1', 0, ['typecheck'], ['typecheck', 'package']),
witness('#2', 's2', 10, [], ['package'])
])
expect(compareVerdict(classification).verdict).toBe(VERDICT.unknown)
})
})
describe('selectWitnessesInWindow', () => {
const from = new Date(T0)
const to = new Date(T0 + HOUR)
it('keeps a witness whose CI completed inside the window', () => {
expect(selectWitnessesInWindow([witness('#1', 's1', 30, [])], from, to)).toHaveLength(1)
})
it('drops a witness whose CI completed before the window', () => {
expect(selectWitnessesInWindow([witness('#1', 's1', -30, [])], from, to)).toEqual([])
})
it('drops a witness with no completion time rather than assuming it fits', () => {
expect(selectWitnessesInWindow([{ ref: '#1', completedAt: null }], from, to)).toEqual([])
})
})
+379
View File
@@ -0,0 +1,379 @@
import { execFile, execFileSync } from 'node:child_process'
import process from 'node:process'
import { pathToFileURL } from 'node:url'
import {
MAX_EVIDENCE_SPAN_HOURS,
VERDICT,
buildStacks,
classifyChecks,
compareVerdict,
mainHealthVerdict,
normalizeChecks,
selectWitnessesInWindow
} from './upstream-breakage-evidence.mjs'
// CLI over the upstream-breakage evidence model: talks to gh and git, prints the
// witness table, and reports a verdict. See
// docs/reference/upstream-breakage-diagnosis.md.
const GH_BUFFER_BYTES = 32 * 1024 * 1024
// Enough parallelism to keep a 40-witness window inside a few seconds without
// tripping GitHub's secondary rate limits.
const FETCH_CONCURRENCY = 8
function gh(args) {
return execFileSync('gh', args, { encoding: 'utf8', maxBuffer: GH_BUFFER_BYTES })
}
function ghAsync(args) {
return new Promise((resolve, reject) => {
execFile('gh', args, { encoding: 'utf8', maxBuffer: GH_BUFFER_BYTES }, (error, stdout) => {
if (error) {
reject(error)
return
}
resolve(stdout)
})
})
}
function git(args) {
return execFileSync('git', args, { encoding: 'utf8' }).trim()
}
const CHECK_RUN_JQ =
'.check_runs[] | {name, status, conclusion, completedAt: .completed_at, appSlug: .app.slug}'
async function fetchCheckRuns(sha, { allAttempts }) {
const filter = allAttempts === true ? 'all' : 'latest'
const raw = await ghAsync([
'api',
`repos/{owner}/{repo}/commits/${sha}/check-runs?per_page=100&filter=${filter}`,
'--paginate',
'--jq',
CHECK_RUN_JQ
])
return raw
.split('\n')
.filter((line) => line.trim() !== '')
.map((line) => JSON.parse(line))
}
// Bounded-concurrency map; keeps the probe in the seconds range for wide windows.
async function mapWithConcurrency(items, worker) {
const results = Array.from({ length: items.length })
let next = 0
const runners = Array.from({ length: Math.min(FETCH_CONCURRENCY, items.length) }, async () => {
while (next < items.length) {
const index = next
next += 1
results[index] = await worker(items[index])
}
})
await Promise.all(runners)
return results
}
const PR_FIELDS = 'number,headRefName,headRefOid,baseRefName,mergedAt,title,url'
// Always passes an explicit limit and reports the count, because `gh pr list`
// silently truncates at 30 and a truncated population reads as a complete one.
function listPullRequests(extraArgs, limit) {
const prs = JSON.parse(
gh(['pr', 'list', '--limit', String(limit), '--json', PR_FIELDS, ...extraArgs])
)
return { prs, truncated: prs.length >= limit, limit }
}
function viewPullRequest(number) {
return JSON.parse(gh(['pr', 'view', String(number), '--json', PR_FIELDS]))
}
function resolveCommit(rev) {
// Never derive a SHA from an abbreviation by hand; let git verify it.
return git(['rev-parse', '--verify', `${rev}^{commit}`])
}
async function witnessesFromPrs(prs, options) {
return mapWithConcurrency(prs, async (pr) => ({
ref: `#${pr.number}`,
pr,
...normalizeChecks(await fetchCheckRuns(pr.headRefOid, options), options)
}))
}
function attachStacks(witnesses) {
const stacks = buildStacks(witnesses.map((w) => w.pr))
const stackOf = new Map()
for (const [index, members] of stacks.entries()) {
for (const number of members) {
stackOf.set(number, `stack-${index + 1}`)
}
}
return witnesses.map((w) => ({ ...w, stackId: stackOf.get(w.pr.number) ?? w.ref }))
}
function describeWitness(w) {
if (!w.usable) {
if (w.incomplete > 0) {
return `no verdict (${w.incomplete} check(s) not completed)`
}
return w.ran.length === 0
? 'no verdict (nothing ran; path filters skipped every job)'
: `no verdict (no lane exercised the tree; only ${w.ran.join(', ')} ran)`
}
return w.failures.length === 0
? `green (${w.ran.length} checks ran)`
: `RED: ${w.failures.join(', ')}`
}
function printWitnesses(witnesses) {
// Chronological: a main-side break shows up as greens above and reds below.
const ordered = [...witnesses].sort((a, b) =>
String(a.completedAt).localeCompare(String(b.completedAt))
)
for (const w of ordered) {
const at =
w.completedAt === null || w.completedAt === undefined
? '?'
: clock(new Date(w.completedAt).getTime())
console.log(` ${at} ${w.ref} [${w.stackId}] ${describeWitness(w)}`)
const dropped = [
...new Set([
...w.excluded.rollup.map((n) => `${n} (roll-up)`),
...w.excluded.knownFalse.map((n) => `${n} (known-false red)`),
...w.excluded.foreignApp.map((n) => `${n} (third-party app)`),
// Printed so a renamed lane that dropped out of the witnessing list is
// visible rather than quietly costing the run its evidence.
...(w.excluded.nonWitnessing ?? []).map((n) => `${n} (does not exercise the tree)`)
])
]
if (dropped.length > 0) {
console.log(` excluded: ${dropped.join('; ')}`)
}
}
}
function clock(ms) {
return ms === null ? '?' : `${new Date(ms).toISOString().replace('T', ' ').slice(0, 16)}Z`
}
function printClassification(c) {
if (c.upstream.length > 0) {
console.log('\nRed in every witness that ran it — main was broken for this whole window:')
for (const u of c.upstream) {
console.log(` ${u.name} — red in ${u.redRefs.join(', ')}`)
}
}
if (c.transitions.length > 0) {
console.log('\nRed for one unbroken stretch — main was broken across that stretch:')
for (const t of c.transitions) {
const opened = Number.isFinite(t.lastGreenBefore)
? `after ${clock(t.lastGreenBefore)}`
: 'before the window'
const closed = Number.isFinite(t.firstGreenAfter)
? `by ${clock(t.firstGreenAfter)}`
: 'still open'
console.log(
` ${t.name} — red ${clock(t.firstRed)}..${clock(t.lastRed)} (broke ${opened}; fixed ${closed})`
)
console.log(` red: ${t.redRefs.join(', ')}`)
}
}
if (c.branchSpecific.length > 0) {
console.log('\nReds and greens interleave in time — branch-specific, not main:')
for (const b of c.branchSpecific) {
console.log(
` ${b.name} — red in ${b.redRefs.join(', ')}; green in ${b.greenRefs.join(', ')}`
)
}
}
if (c.inconclusive.length > 0) {
console.log('\nSeen too narrowly to attribute:')
for (const i of c.inconclusive) {
console.log(` ${i.name} — red in ${i.redRefs.join(', ')}; ${i.reason}`)
}
}
}
export function parseArgs(argv) {
const options = {
mode: argv[0] ?? null,
refs: [],
commit: null,
windowHours: 3,
limit: 100,
maxSpanHours: MAX_EVIDENCE_SPAN_HOURS,
allAttempts: false,
includeKnownFalse: false,
json: false
}
const rest = argv.slice(1)
while (rest.length > 0) {
const arg = rest.shift()
if (arg === '--window-hours') {
options.windowHours = Number(rest.shift())
} else if (arg === '--limit') {
options.limit = Number(rest.shift())
} else if (arg === '--max-span-hours') {
options.maxSpanHours = Number(rest.shift())
} else if (arg === '--all-attempts') {
options.allAttempts = true
} else if (arg === '--include-known-false') {
options.includeKnownFalse = true
} else if (arg === '--json') {
options.json = true
} else if (arg.startsWith('-')) {
throw new Error(`unknown flag: ${arg}`)
} else if (options.mode === 'at' && options.commit === null) {
options.commit = arg
} else {
options.refs.push(arg.replace(/^#/, ''))
}
}
return options
}
const USAGE = `Usage:
node config/scripts/upstream-breakage-probe.mjs at <commit> [--window-hours N] [--limit N]
node config/scripts/upstream-breakage-probe.mjs compare <pr-number> ...
at Was main broken at/near this commit? Reconstructed from the PRs whose
CI ran against main in the surrounding window.
compare Do these red branches share one failure set (upstream) or not?
Flags:
--window-hours N witness window for \`at\` (default 3)
--limit N explicit gh pr list limit (default 100)
--max-span-hours N reject evidence spread wider than this (default ${MAX_EVIDENCE_SPAN_HOURS})
--all-attempts include earlier check attempts a re-run to green hid
--include-known-false count the known-false reds instead of excluding them
--json machine-readable output
Verdicts never default to pass: without enough evidence the answer is "unknown".`
function describeSpan(classification) {
return classification.evidenceSpanHours === null
? 'evidence span: unknown (fewer than two timestamps)'
: `evidence span: ${classification.evidenceSpanHours.toFixed(1)}h`
}
async function runCompare(options) {
if (options.refs.length === 0) {
throw new Error('compare needs at least one PR number')
}
const selected = options.refs.map(Number).map(viewPullRequest)
const witnesses = attachStacks(await witnessesFromPrs(selected, options))
const classification = classifyChecks(witnesses)
const verdict = compareVerdict(classification, options)
if (options.json) {
console.log(JSON.stringify({ mode: 'compare', verdict, classification, witnesses }, null, 2))
return
}
console.log(
`Compared ${witnesses.length} ref(s) across ${classification.independentStacks} independent stack(s); ${describeSpan(classification)}.`
)
printWitnesses(witnesses)
printClassification(classification)
console.log(`\nVERDICT: ${verdict.verdict} — ${verdict.why}`)
if (verdict.verdict === VERDICT.upstream) {
console.log('Next step: merge a newer main into each branch. Do not repair the branches.')
}
if (verdict.verdict === VERDICT.sharedAncestor) {
console.log(
'Next step: compare against a red PR from a different stack, or probe the stack root with `at`.'
)
}
}
async function runAt(options) {
if (options.commit === null) {
throw new Error('at needs a commit')
}
const sha = resolveCommit(options.commit)
const when = new Date(git(['show', '-s', '--format=%cI', sha]))
const windowMs = options.windowHours * 3600 * 1000
const from = new Date(when.getTime() - windowMs)
const to = new Date(when.getTime() + windowMs)
const { prs, truncated, limit } = listPullRequests(['--state', 'merged'], options.limit)
// CI completes before the merge, so preselect generously and let the
// completion-time filter below decide.
const candidates = prs.filter((pr) => {
if (pr.mergedAt === null || pr.mergedAt === undefined) {
return false
}
const merged = new Date(pr.mergedAt).getTime()
return merged >= from.getTime() && merged <= to.getTime() + windowMs
})
const fetched = await witnessesFromPrs(candidates, options)
const witnesses = attachStacks(selectWitnessesInWindow(fetched, from, to))
const classification = classifyChecks(witnesses)
const verdict = mainHealthVerdict(classification, when.getTime(), options)
if (options.json) {
console.log(
JSON.stringify(
{ mode: 'at', sha, window: { from, to }, verdict, classification, witnesses },
null,
2
)
)
return
}
console.log(`main @ ${sha} (${when.toISOString()})`)
console.log(
`Scanned ${prs.length} merged PR(s) at gh limit ${limit}; ${candidates.length} merged near the window, ${witnesses.length} with CI inside ±${options.windowHours}h, ${classification.usableWitnesses} usable across ${classification.independentStacks} independent stack(s); ${describeSpan(classification)}.`
)
if (truncated) {
console.log(
` WARNING: gh pr list returned exactly ${limit} rows (the limit); the window may be truncated — raise --limit.`
)
}
printWitnesses(witnesses)
printClassification(classification)
console.log(`\nVERDICT: main was ${verdict.verdict} at this commit — ${verdict.why}`)
if (verdict.verdict === VERDICT.broken) {
console.log(
'A branch red on these checks is inheriting main. Merge a newer main; do not repair the branch.'
)
}
if (verdict.verdict === VERDICT.unknown) {
console.log(
'Unknown is not green. Widen --window-hours, or use `compare` on the red branches directly.'
)
}
}
async function main(argv) {
let options
try {
options = parseArgs(argv)
} catch (error) {
console.error(`${error.message}\n\n${USAGE}`)
process.exitCode = 2
return
}
if (options.mode === 'at') {
await runAt(options)
return
}
if (options.mode === 'compare') {
await runCompare(options)
return
}
console.error(USAGE)
process.exitCode = 2
}
if (import.meta.url === pathToFileURL(process.argv[1] ?? '').href) {
main(process.argv.slice(2)).catch((error) => {
console.error(error.message ?? error)
process.exitCode = 1
})
}
@@ -0,0 +1,150 @@
# Telling "my merge broke it" apart from "main was already broken"
Read this before diagnosing a branch that went red after merging `main`, and
before writing a repair brief for a stack that went red together.
## The mistake this exists to prevent
An eleven-PR chain merged `main` inside a window during which `main` itself was
broken: a dependency bump had broken a group of test shards, and a lint violation
was already sitting on `main`. Six of the eleven nodes went red. The reds were
diagnosed as damage caused by the merge, written up, and acted on. All of it was
wrong — the fix was to merge a newer `main`.
The tell was already in the data: **an identical failure multiset on every node,
including the root**. Damage from a merge varies with what each node changed.
Identical failures across an entire chain are upstream by construction.
## Why you cannot just look up main's CI
The obvious move is to ask GitHub whether `main` was green at a SHA. In this repo
that question has no answer, and it is worth knowing why before you go looking:
- `pr.yml` — the workflow that holds the tests, typecheck, static analysis and
packaging — is `on: pull_request` only.
- `unit-tests.yml` is `workflow_call` only; nothing calls it from a push.
- No workflow runs the test suite on a push to `main`. The only lanes that ever
execute on `main`'s own commits are scheduled or manually dispatched (E2E, the
mac builds, terminal perf, node-next), plus a path-filtered skill round-trip.
The result is that a commit on `main` carries **zero** check runs:
```
$ gh api "repos/{owner}/{repo}/commits/<main-sha>/check-runs" --jq .total_count
0
```
The one lane that does run on `main` is scheduled E2E, twice a day, and it has
been uniformly red for weeks — so it cannot discriminate either.
So `main`'s health has to be reconstructed from the PRs whose CI ran against
`main` at that moment. That is what the probe does.
## What to run
```sh
# Was main broken at or near this commit?
pnpm run diagnose:upstream-breakage at <commit> --window-hours 1
# Are these red branches red for the same reason?
pnpm run diagnose:upstream-breakage compare 17330 17331 17332
```
Both finish in a few seconds. Both take `--json`.
`at` resolves the commit, finds the PRs whose checks completed in the surrounding
window, and reports each check's behaviour over time. `compare` does the same for
a list of PRs you name.
## How it decides
Every PR on which **a lane that actually executes the tree** ran in the window is
a **witness**. That qualification is the whole load-bearing part: a path-filtered
PR — docs-only, mobile-only — skips every lane in `pr.yml` and still posts three
green jobs (the path classifier, the root-directory guard and the LoC counter).
Those stay green on a `main` that is entirely broken, so a PR carrying only them
witnessed nothing and the probe refuses to count it. The list of qualifying lanes
is an allowlist in `upstream-breakage-evidence.mjs`: a name it does not recognise
costs a witness and pushes the verdict toward `unknown`, rather than buying one
and pushing it toward `clean`. Every unrecognised name is printed on each run.
For each check name the probe then looks at when it was red and when it was green:
| Shape | Meaning |
| --------------------------------------------------------- | ------------------------------------------------------------------------------------ |
| Red in every witness that ran it | `main` was broken for the whole window |
| Red across one unbroken stretch of time, green outside it | `main` broke at the start of that stretch, and was fixed at the end if greens follow |
| Reds and greens interleave in time | branch-specific — the tree was fine between the reds |
| Red in fewer than two independent stacks | not attributable either way |
Two PRs in the same stack share a diff, so they count as one witness, not two.
The probe reconstructs stacks from branch parentage: a PR whose base branch is
another PR's head branch is stacked on it, **and PRs that share one non-trunk base
branch are one stack even when that branch has no PR of its own** — otherwise two
children of an unlisted parent corroborate each other, which is one witness
counted twice.
The output for the real incident looks like this — the exact window, named:
```
test / tests node 24 2/8 — red 04:09Z..04:23Z (broke after 04:05Z; fixed by 04:39Z)
```
## It says unknown, and unknown is not green
The probe answers `broken`, `clean`, or `unknown`, and it will not say `clean`
without positive evidence. It answers `unknown` when:
- fewer than two PRs ran a lane that exercises the tree — including the case where
every PR in the window was path-filtered down to its meta jobs;
- the witnesses are all in one stack, so they corroborate nothing;
- the witness checks are spread over more than a day, so they ran against
different `main`s and their agreement means nothing;
- the commit sits in a blind gap — between the last green and the first red — so
no witness observed `main` there;
- a failing check was seen too narrowly to attribute.
This is the same discipline as the `live` / `unverifiable` / `exited` vocabulary
in [`ssh-execution-boundary.md`](./ssh-execution-boundary.md). A health check
that guesses green is exactly the failure it is meant to catch.
## What it deliberately does not count
Printed on every run, never dropped silently:
- **`verify`** is a roll-up job that reprints another job's failure. Counting it
double-counts one breakage and makes divergent failure sets look identical.
- **Known-false reds** — `test / tests node 24 1/8`, `test / tests node 24 6/8`,
and `e2e / ssh docker watcher isolation` — are red for reasons unrelated to the
commit. Counting them makes every window look broken. `--include-known-false`
overrides this.
- **Third-party app checks** (review bots) say nothing about the tree.
- **Skipped checks** are not passes, and neither are the always-on jobs that never
execute the tree — `detect code-relevant changes`, `root directory guard`,
`test vs non-test LoC`, `track-community-pr`, and the e2e spec-list classifiers.
A PR left with only those is not a witness at all, so a window made up of
path-filtered PRs reads `unknown`, not `clean`.
- **`verify`** is also the job name of the mobile workflow's only lane, so a
mobile-only PR loses that check to the roll-up exclusion as well. It is not a
witness for `main`'s desktop health either way.
## Traps to keep in mind
- `gh pr list` and `gh run list` **truncate silently** — the default is 30 rows.
The probe always passes an explicit `--limit`, prints the count it scanned, and
warns when the result came back exactly at the limit. If you query by hand, do
the same, and print the count beside any conclusion.
- A run's `conclusion` **hides failed first attempts**: a run re-run to green
reads `success`. Pass `--all-attempts` when attempt-level truth matters.
- Read `main` and the workflow definitions from `origin/main`, not from a
worktree. A checkout fourteen commits behind gives a confidently wrong answer.
- The probe cannot see which base SHA each PR's CI actually merged against, only
when it ran. Two checks minutes apart can still straddle a merge. That is why
a narrow `--window-hours` sharpens the answer and why blind gaps read `unknown`.
## When it says upstream
Merge a newer `main` into each branch. Do not repair the branches, do not write
the failures up as merge damage, and do not rebase the chain — see
[`git-compatibility.md`](./git-compatibility.md) for the Git-side constraints on
restacking.
+1
View File
@@ -20,6 +20,7 @@
"check:code-quality:changed": "node config/scripts/check-changed-code-quality.mjs",
"check:react-doctor:changed": "node config/scripts/check-react-doctor-changed.mjs",
"check:zustand-selector-fanout": "node config/scripts/zustand-selector-fanout-benchmark.mjs --check",
"diagnose:upstream-breakage": "node config/scripts/upstream-breakage-probe.mjs",
"doctor": "pnpm dlx react-doctor@0.9.1 . --no-telemetry",
"lint:react-doctor": "oxlint --config config/oxlint-react-doctor.json",
"lint:react-doctor:changed": "node config/scripts/lint-react-doctor-changed.mjs",