refactor(preload): split the preload contract into per-domain api modules (#14403)

`src/preload/api-types.ts` was 3,752 raw lines (3,533 counted, 11.8x the
300-line budget) behind an `eslint-disable max-lines`. Almost all of it was a
single `PreloadApi` object type whose ~83 namespace properties were declared
inline, so any IPC surface change meant editing one 2,600-line type.

Give each namespace a named type in its own module under `src/preload/api/`
(`pty-api.ts`, `filesystem-api.ts`, `github-pull-request-api.ts`, ...) and
recompose `PreloadApi` from those names. `api-types.ts` keeps the `declare
global` Window augmentation and re-exports every moved name, so all 52 import
sites are untouched.

Two shapes needed care to stay type-identical rather than merely compatible:

- Three keys (`gh`, `git`, `ui`) are composed from two modules each. A plain
  intersection is NOT identical to the original flat object literal, so those
  use a `Merged<T>` mapped type; a negative control confirmed that dropping it
  fails the parity assertion.
- Keys whose module groups several namespaces use indexed access
  (`fs: FilesystemApi['fs']`) to preserve exact identity and source order.

`config/tsconfig.web.json` and `tsconfig.tc.web.json` enumerate files by path,
so they need `src/preload/api/**/*` alongside the existing `api-types.ts` seed
or the web projects fail TS6307.

Verified by exact type identity, not assignability: 41 assertions of the form
`Equals<Now.X, Before.X>` against a frozen pre-split snapshot, covering every
exported name, plus a per-key pass over all 83 `PreloadApi` keys. All three
projects typecheck clean with those assertions active.

Verification note: these tsconfigs are `composite: true`, and `tsc --noEmit`
will reuse a stale `.tsbuildinfo` and report clean for a state that genuinely
fails. Every result above was produced after deleting the buildinfo, including
a negative control confirming the gate still fails on deliberate drift.

Drops the `max-lines` bypass and its baseline entry (ratchet 346 -> 345).
This commit is contained in:
Neil
2026-08-13 20:52:04 -07:00
committed by GitHub
parent 583ab1601b
commit 4221f8d429
51 changed files with 4040 additions and 3723 deletions
+196
View File
@@ -0,0 +1,196 @@
import type { AppStarSource } from '../../shared/gh-star-source'
import type { GhAuthDiagnostic } from '../../shared/github/auth-types'
import type { TaskSourceContext } from '../../shared/task-source-context'
import type {
GetRateLimitResult,
GitHubCommentResult,
GitHubOwnerRepo,
GitHubPRFile,
GitHubPRFileContents,
GitHubPRRefreshCandidate,
GitHubPRRefreshEnqueueResult,
GitHubPRRefreshEvent,
GitHubPRRefreshReason,
GitHubPRReviewCommentInput,
GitHubReactionContent,
GitHubViewer,
PRCheckDetail,
PRCheckRunDetails,
PRComment,
PRInfo,
PRRefreshOutcome
} from '../../shared/types'
import type { GitHubRepoSelectorArgs } from './github-work-item-api'
export type GithubPullRequestApi = {
viewer: () => Promise<GitHubViewer | null>
repoSlug: (args: {
repoPath: string
repoId?: string
}) => Promise<{ owner: string; repo: string; host?: string } | null>
repoUpstream: (args: {
repoPath: string
repoId?: string
}) => Promise<{ owner: string; repo: string; host?: string } | null>
prForBranch: (args: {
repoPath: string
repoId?: string
branch: string
linkedPRNumber?: number | null
fallbackPRNumber?: number | null
acceptMergedFallbackPR?: boolean
currentHeadOid?: string | null
}) => Promise<PRInfo | null>
refreshPRNow: (args: { candidate: GitHubPRRefreshCandidate }) => Promise<PRRefreshOutcome>
enqueuePRRefresh: (args: {
candidate: GitHubPRRefreshCandidate
reason: GitHubPRRefreshReason
priority?: number
}) => Promise<GitHubPRRefreshEnqueueResult | false>
reportVisiblePRRefreshCandidates: (args: {
candidates: GitHubPRRefreshCandidate[]
generation: number
}) => Promise<boolean>
onPRRefreshEvent: (callback: (event: GitHubPRRefreshEvent) => void) => () => void
prFileContents: (
args: GitHubRepoSelectorArgs & {
prNumber: number
prRepo?: GitHubOwnerRepo | null
path: string
oldPath?: string
status: GitHubPRFile['status']
headSha: string
baseSha: string
}
) => Promise<GitHubPRFileContents>
prChecks: (
args: GitHubRepoSelectorArgs & {
prNumber: number
headSha?: string
prRepo?: GitHubOwnerRepo | null
noCache?: boolean
}
) => Promise<PRCheckDetail[]>
prCheckDetails: (args: {
repoPath: string
repoId?: string
sourceContext?: TaskSourceContext | null
checkRunId?: number
workflowRunId?: number
checkName?: string
url?: string | null
prRepo?: GitHubOwnerRepo | null
}) => Promise<PRCheckRunDetails | null>
rerunPRChecks: (
args: GitHubRepoSelectorArgs & {
prNumber: number
headSha?: string
failedOnly?: boolean
prRepo?: GitHubOwnerRepo | null
}
) => Promise<{ ok: true; count: number } | { ok: false; error: string }>
prComments: (args: {
repoPath: string
repoId?: string
sourceContext?: TaskSourceContext | null
prNumber: number
prRepo?: GitHubOwnerRepo | null
noCache?: boolean
}) => Promise<PRComment[]>
setPRCommentReaction: (args: {
repoPath: string
repoId?: string
sourceContext?: TaskSourceContext | null
reactionSubjectId: string
content: GitHubReactionContent
reacted: boolean
prRepo?: GitHubOwnerRepo | null
}) => Promise<boolean>
resolveReviewThread: (args: {
repoPath: string
repoId?: string
sourceContext?: TaskSourceContext | null
threadId: string
resolve: boolean
prRepo?: GitHubOwnerRepo | null
}) => Promise<boolean>
setPRFileViewed: (
args: GitHubRepoSelectorArgs & {
prNumber: number
prRepo?: GitHubOwnerRepo | null
pullRequestId: string
path: string
viewed: boolean
}
) => Promise<boolean>
updatePRTitle: (args: {
repoPath: string
repoId?: string
prNumber: number
title: string
prRepo?: GitHubOwnerRepo | null
}) => Promise<boolean>
mergePR: (
args: GitHubRepoSelectorArgs & {
prNumber: number
method?: 'merge' | 'squash' | 'rebase'
prRepo?: GitHubOwnerRepo | null
}
) => Promise<{ ok: true } | { ok: false; error: string }>
setPRAutoMerge: (
args: GitHubRepoSelectorArgs & {
prNumber: number
enabled: boolean
method?: 'merge' | 'squash' | 'rebase'
prRepo?: GitHubOwnerRepo | null
}
) => Promise<{ ok: true } | { ok: false; error: string }>
updatePRState: (
args: GitHubRepoSelectorArgs & {
prNumber: number
updates: { state: 'open' | 'closed' }
prRepo?: GitHubOwnerRepo | null
}
) => Promise<{ ok: true } | { ok: false; error: string }>
requestPRReviewers: (
args: GitHubRepoSelectorArgs & {
prNumber: number
reviewers: string[]
prRepo?: GitHubOwnerRepo | null
}
) => Promise<{ ok: true } | { ok: false; error: string }>
removePRReviewers: (
args: GitHubRepoSelectorArgs & {
prNumber: number
reviewers: string[]
prRepo?: GitHubOwnerRepo | null
}
) => Promise<{ ok: true } | { ok: false; error: string }>
addPRReviewCommentReply: (
args: GitHubRepoSelectorArgs & {
prNumber: number
commentId: number
body: string
threadId?: string
path?: string
line?: number
prRepo?: GitHubOwnerRepo | null
}
) => Promise<GitHubCommentResult>
addPRReviewComment: (
args: GitHubPRReviewCommentInput & {
repoId?: string
sourceContext?: TaskSourceContext | null
}
) => Promise<GitHubCommentResult>
checkOrcaStarred: () => Promise<boolean | null>
starOrca: (source: AppStarSource) => Promise<boolean>
/**
* GitHub API rate-limit snapshot. Does NOT consume quota (the
* `rate_limit` endpoint is exempt). Cached 30s server-side — pass
* `force: true` to bust after a known-expensive op.
*/
rateLimit: (args?: { force?: boolean }) => Promise<GetRateLimitResult>
/** Explains scope_missing ProjectV2 failures — notably a shell `GITHUB_TOKEN` shadowing the keyring credential, where `gh auth refresh` is a no-op. */
diagnoseAuth: (args?: { host?: string }) => Promise<GhAuthDiagnostic>
}