mirror of
https://github.com/stablyai/orca.git
synced 2026-10-02 16:02:15 +00:00
#14397 split `shared/types.ts` into 46 per-domain modules but kept the path as a re-export barrel so the import sites did not have to change. This removes the barrel: every consumer now imports from the module that actually declares the type, and `src/shared/types.ts` is deleted. Barrels hide where a type lives, make every consumer look like it depends on the whole domain, and let an unrelated edit invalidate a module that ~2,000 files transitively import. 2,323 import declarations across 2,321 files. Rewritten mechanically: each specifier was resolved to an absolute path via the TypeScript AST and recomputed, rather than string-substituted, so alias forms (`@/../../shared/ types`) and per-specifier `type` modifiers survive. Four cases the mechanical pass had to handle, each found by a gate rather than by reading the diff: - Modules inside `src/shared` import the barrel as `./types`, not `shared/types`. A pre-filter on the latter string skipped 176 of them and left imports dangling at a deleted file, which surfaced as confusing `Property 'x' is optional in type 'Repo' but required in Pick<Repo, ...>` errors rather than "module not found". - The barrel RENAMED one type on the way through (`WorkspaceSource as WorkspaceCreateTelemetrySource`), so the original name in the owning module has to be re-aliased at each consumer. - Three test files put `;(globalThis as ...)` on the line after the import. TypeScript parses that `;` as the import statement's terminator, so replacing through `statement.getEnd()` deletes it and breaks ASI. The rewrite now stops at the module specifier. - A file that already imported directly from a module got a SECOND import from it, because the barrel re-exported those same names — which trips `import/no-duplicates` under `--deny-warnings`. A post-pass merges declarations sharing a specifier and type-only-ness; the `import type` plus `import` pair from one module is left alone, since that form is allowed. Splitting one barrel import into several genuinely adds lines, which pushed `terminal-layout-pty-ownership.ts` to 301 counted lines: its 107-character import must wrap, and neither local type collapses onto one line (101 and 116 characters). Rather than contort a type declaration to fit a line budget, `collectLeafIds` and `pruneLeaves` move to `terminal-pane-layout-tree.ts` — they are pure structural operations on the layout tree and independent of PTY ownership. `visible-worktrees.ts` similarly loses its own mini-barrel re-export of `isDefaultBranchWorkspace`, with the four real consumers repointed at the declaring module. No `max-lines` bypass added. Verified: cold `tsc --noEmit` green on node, cli, and web (buildinfo deleted first — these projects are `composite: true` and reuse stale caches); the full `pnpm lint` green, not just bare oxlint — the narrower local check is what let the duplicate imports reach CI; max-lines ratchet OK at 344.
283 lines
8.0 KiB
TypeScript
283 lines
8.0 KiB
TypeScript
import type {
|
|
CheckStatus,
|
|
GitHubPRStack,
|
|
GitHubPRStackEntry,
|
|
PRMergeableState,
|
|
PRReviewDecision,
|
|
PRState
|
|
} from '../../shared/github/pull-request-types'
|
|
import { githubRepoIdentityKey } from '../../shared/github/repository-identity-key'
|
|
import { ghExecFileAsync } from '../git/runner'
|
|
import {
|
|
githubHostExecOptions,
|
|
type GitHubApiRepository,
|
|
type GitHubRepoExecOptions
|
|
} from './github-api-repository'
|
|
import { noteRepositoryRateLimitSpend, repositoryRateLimitGuard } from './rate-limit'
|
|
|
|
const STACK_CACHE_TTL_MS = 30_000
|
|
const STACK_CACHE_MAX_ENTRIES = 256
|
|
|
|
type CachedStackDetails = {
|
|
value: Omit<GitHubPRStack, 'position'> | null
|
|
expiresAt: number
|
|
}
|
|
|
|
type GraphQLStackEntry = {
|
|
position?: unknown
|
|
pullRequest?: {
|
|
number?: unknown
|
|
title?: unknown
|
|
url?: unknown
|
|
updatedAt?: unknown
|
|
state?: unknown
|
|
isDraft?: unknown
|
|
headRefName?: unknown
|
|
headRefOid?: unknown
|
|
mergeable?: unknown
|
|
reviewDecision?: unknown
|
|
mergeStateStatus?: unknown
|
|
statusCheckRollup?: { state?: unknown } | null
|
|
} | null
|
|
}
|
|
|
|
type GraphQLStackResponse = {
|
|
data?: {
|
|
repository?: {
|
|
pullRequest?: {
|
|
stack?: {
|
|
number?: unknown
|
|
size?: unknown
|
|
baseRefName?: unknown
|
|
entries?: { nodes?: (GraphQLStackEntry | null)[] | null } | null
|
|
} | null
|
|
} | null
|
|
} | null
|
|
}
|
|
}
|
|
|
|
const stackDetailsCache = new Map<string, CachedStackDetails>()
|
|
const stackDetailsInFlight = new Map<string, Promise<Omit<GitHubPRStack, 'position'> | null>>()
|
|
|
|
export function _resetGitHubPRStackCacheForTests(): void {
|
|
stackDetailsCache.clear()
|
|
stackDetailsInFlight.clear()
|
|
}
|
|
|
|
function stackCacheKey(
|
|
repository: GitHubApiRepository,
|
|
stackNumber: number,
|
|
executionScope: string
|
|
): string {
|
|
return `${executionScope}\0${githubRepoIdentityKey(repository)}:${stackNumber}`
|
|
}
|
|
|
|
function pruneStackCache(now = Date.now()): void {
|
|
for (const [key, cached] of stackDetailsCache) {
|
|
if (cached.expiresAt <= now) {
|
|
stackDetailsCache.delete(key)
|
|
}
|
|
}
|
|
while (stackDetailsCache.size > STACK_CACHE_MAX_ENTRIES) {
|
|
const oldestKey = stackDetailsCache.keys().next().value
|
|
if (oldestKey === undefined) {
|
|
return
|
|
}
|
|
stackDetailsCache.delete(oldestKey)
|
|
}
|
|
}
|
|
|
|
function mapStackPRState(value: unknown, isDraft: unknown): PRState {
|
|
if (value === 'MERGED') {
|
|
return 'merged'
|
|
}
|
|
if (value === 'CLOSED') {
|
|
return 'closed'
|
|
}
|
|
return isDraft === true ? 'draft' : 'open'
|
|
}
|
|
|
|
function mapStackCheckStatus(value: unknown): CheckStatus {
|
|
if (value === 'SUCCESS') {
|
|
return 'success'
|
|
}
|
|
if (value === 'FAILURE' || value === 'ERROR') {
|
|
return 'failure'
|
|
}
|
|
if (value === 'PENDING' || value === 'EXPECTED') {
|
|
return 'pending'
|
|
}
|
|
return 'neutral'
|
|
}
|
|
|
|
function mapStackMergeable(value: unknown): PRMergeableState {
|
|
return value === 'MERGEABLE' || value === 'CONFLICTING' ? value : 'UNKNOWN'
|
|
}
|
|
|
|
function mapReviewDecision(value: unknown): PRReviewDecision | null | undefined {
|
|
if (value === null) {
|
|
return null
|
|
}
|
|
if (value === 'APPROVED' || value === 'CHANGES_REQUESTED' || value === 'REVIEW_REQUIRED') {
|
|
return value
|
|
}
|
|
return undefined
|
|
}
|
|
|
|
function mapStackEntry(entry: GraphQLStackEntry): GitHubPRStackEntry | null {
|
|
const pr = entry.pullRequest
|
|
if (
|
|
typeof entry.position !== 'number' ||
|
|
typeof pr?.number !== 'number' ||
|
|
typeof pr.title !== 'string' ||
|
|
typeof pr.url !== 'string'
|
|
) {
|
|
return null
|
|
}
|
|
const reviewDecision = mapReviewDecision(pr.reviewDecision)
|
|
return {
|
|
position: entry.position,
|
|
number: pr.number,
|
|
title: pr.title,
|
|
url: pr.url,
|
|
...(typeof pr.updatedAt === 'string' ? { updatedAt: pr.updatedAt } : {}),
|
|
state: mapStackPRState(pr.state, pr.isDraft),
|
|
checksStatus: mapStackCheckStatus(pr.statusCheckRollup?.state),
|
|
mergeable: mapStackMergeable(pr.mergeable),
|
|
...(reviewDecision !== undefined ? { reviewDecision } : {}),
|
|
...(typeof pr.mergeStateStatus === 'string' ? { mergeStateStatus: pr.mergeStateStatus } : {}),
|
|
...(typeof pr.headRefName === 'string' ? { headRefName: pr.headRefName } : {}),
|
|
...(typeof pr.headRefOid === 'string' ? { headSha: pr.headRefOid } : {})
|
|
}
|
|
}
|
|
|
|
function parseStackDetails(
|
|
response: GraphQLStackResponse,
|
|
expectedStackNumber: number,
|
|
fallbackBaseRefName: string,
|
|
fallbackSize: number
|
|
): Omit<GitHubPRStack, 'position'> | null {
|
|
const stack = response.data?.repository?.pullRequest?.stack
|
|
if (!stack || stack.number !== expectedStackNumber) {
|
|
return null
|
|
}
|
|
const entries = (stack.entries?.nodes ?? [])
|
|
.flatMap((entry) => (entry ? [mapStackEntry(entry)] : []))
|
|
.filter((entry): entry is GitHubPRStackEntry => entry !== null)
|
|
.sort((a, b) => a.position - b.position)
|
|
return {
|
|
number: expectedStackNumber,
|
|
size: typeof stack.size === 'number' ? stack.size : fallbackSize,
|
|
baseRefName: typeof stack.baseRefName === 'string' ? stack.baseRefName : fallbackBaseRefName,
|
|
...(entries.length > 0 ? { entries } : {})
|
|
}
|
|
}
|
|
|
|
const STACK_DETAILS_QUERY = `
|
|
query($owner: String!, $repo: String!, $pr: Int!) {
|
|
repository(owner: $owner, name: $repo) {
|
|
pullRequest(number: $pr) {
|
|
stack {
|
|
number
|
|
size
|
|
baseRefName
|
|
entries(first: 100) {
|
|
nodes {
|
|
position
|
|
pullRequest {
|
|
number
|
|
title
|
|
url
|
|
updatedAt
|
|
state
|
|
isDraft
|
|
headRefName
|
|
headRefOid
|
|
mergeable
|
|
reviewDecision
|
|
mergeStateStatus
|
|
statusCheckRollup { state }
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}`
|
|
|
|
async function fetchStackDetails(
|
|
repository: GitHubApiRepository,
|
|
prNumber: number,
|
|
summary: GitHubPRStack,
|
|
ghOptions: GitHubRepoExecOptions
|
|
): Promise<Omit<GitHubPRStack, 'position'> | null> {
|
|
if (repositoryRateLimitGuard(repository, 'graphql', ghOptions).blocked) {
|
|
return null
|
|
}
|
|
noteRepositoryRateLimitSpend(repository, 'graphql', 1, ghOptions)
|
|
const { stdout } = await ghExecFileAsync(
|
|
[
|
|
'api',
|
|
'graphql',
|
|
'-f',
|
|
`query=${STACK_DETAILS_QUERY}`,
|
|
'-f',
|
|
`owner=${repository.owner}`,
|
|
'-f',
|
|
`repo=${repository.repo}`,
|
|
'-F',
|
|
`pr=${prNumber}`
|
|
],
|
|
{ ...ghOptions, ...githubHostExecOptions(repository) }
|
|
)
|
|
return parseStackDetails(
|
|
JSON.parse(stdout) as GraphQLStackResponse,
|
|
summary.number,
|
|
summary.baseRefName,
|
|
summary.size
|
|
)
|
|
}
|
|
|
|
export async function hydrateGitHubPRStack(
|
|
repository: GitHubApiRepository,
|
|
prNumber: number,
|
|
summary: GitHubPRStack,
|
|
ghOptions: GitHubRepoExecOptions,
|
|
prUpdatedAt?: string,
|
|
executionScope = 'local:host'
|
|
): Promise<GitHubPRStack> {
|
|
const key = stackCacheKey(repository, summary.number, executionScope)
|
|
const now = Date.now()
|
|
pruneStackCache(now)
|
|
const cached = stackDetailsCache.get(key)
|
|
const cachedPRUpdatedAt = cached?.value?.entries?.find(
|
|
(entry) => entry.number === prNumber
|
|
)?.updatedAt
|
|
const cachedMatchesPR = !prUpdatedAt || !cachedPRUpdatedAt || cachedPRUpdatedAt === prUpdatedAt
|
|
if (cached && cached.expiresAt > now && cachedMatchesPR) {
|
|
return cached.value
|
|
? { ...cached.value, position: summary.position, baseSha: summary.baseSha }
|
|
: summary
|
|
}
|
|
const existing = stackDetailsInFlight.get(key)
|
|
if (existing) {
|
|
const value = await existing
|
|
return value ? { ...value, position: summary.position, baseSha: summary.baseSha } : summary
|
|
}
|
|
const request = fetchStackDetails(repository, prNumber, summary, ghOptions).catch(() => null)
|
|
stackDetailsInFlight.set(key, request)
|
|
try {
|
|
const value = await request
|
|
stackDetailsCache.delete(key)
|
|
stackDetailsCache.set(key, { value, expiresAt: Date.now() + STACK_CACHE_TTL_MS })
|
|
pruneStackCache()
|
|
return value ? { ...value, position: summary.position, baseSha: summary.baseSha } : summary
|
|
} finally {
|
|
if (stackDetailsInFlight.get(key) === request) {
|
|
stackDetailsInFlight.delete(key)
|
|
}
|
|
}
|
|
}
|
|
|
|
export { mergeGitHubPRStack } from './github-pr-stack-async-merge'
|