mirror of
https://github.com/stablyai/orca.git
synced 2026-09-30 08:03:12 +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.
211 lines
6.3 KiB
TypeScript
211 lines
6.3 KiB
TypeScript
import type { PRCheckDetail } from '../../../src/shared/github/check-types'
|
|
import type {
|
|
CheckStatus,
|
|
GitHubAssignableUser,
|
|
GitHubPRMergeMethod,
|
|
GitHubPRMergeMethodSettings,
|
|
GitHubPRReviewSummary,
|
|
GitHubRepositoryIdentity,
|
|
PRMergeableState,
|
|
PRReviewDecision,
|
|
PRState,
|
|
ProviderCheckSummary
|
|
} from '../../../src/shared/github/pull-request-types'
|
|
import type { HostedReviewProvider } from '../../../src/shared/hosted-review'
|
|
|
|
// Primitive + enum value readers shared by the github.* PR parsers. Each narrows
|
|
// `unknown` defensively (never throws) so RPC payloads can be parsed safely.
|
|
|
|
export function isRecord(value: unknown): value is Record<string, unknown> {
|
|
return typeof value === 'object' && value !== null
|
|
}
|
|
|
|
export function readString(value: unknown): string | undefined {
|
|
return typeof value === 'string' ? value : undefined
|
|
}
|
|
|
|
export function readNumber(value: unknown): number | undefined {
|
|
return typeof value === 'number' && Number.isFinite(value) ? value : undefined
|
|
}
|
|
|
|
export function readBoolean(value: unknown): boolean | undefined {
|
|
return typeof value === 'boolean' ? value : undefined
|
|
}
|
|
|
|
export function readStringArray(value: unknown): string[] {
|
|
if (!Array.isArray(value)) {
|
|
return []
|
|
}
|
|
return value.flatMap((entry): string[] => {
|
|
const str = readString(entry)
|
|
return str === undefined ? [] : [str]
|
|
})
|
|
}
|
|
|
|
export function readProvider(value: unknown): HostedReviewProvider | undefined {
|
|
return value === 'github' ||
|
|
value === 'gitlab' ||
|
|
value === 'bitbucket' ||
|
|
value === 'azure-devops' ||
|
|
value === 'gitea' ||
|
|
value === 'unsupported'
|
|
? value
|
|
: undefined
|
|
}
|
|
|
|
export function readPRState(value: unknown): PRState | null {
|
|
return value === 'open' || value === 'closed' || value === 'merged' || value === 'draft'
|
|
? value
|
|
: null
|
|
}
|
|
|
|
export function readCheckStatus(value: unknown): CheckStatus {
|
|
return value === 'pending' || value === 'success' || value === 'failure' || value === 'neutral'
|
|
? value
|
|
: 'pending'
|
|
}
|
|
|
|
export function readMergeableState(value: unknown): PRMergeableState | undefined {
|
|
return value === 'MERGEABLE' || value === 'CONFLICTING' || value === 'UNKNOWN' ? value : undefined
|
|
}
|
|
|
|
export function readReviewDecision(value: unknown): PRReviewDecision | null | undefined {
|
|
if (value === null) {
|
|
return null
|
|
}
|
|
return value === 'APPROVED' || value === 'CHANGES_REQUESTED' || value === 'REVIEW_REQUIRED'
|
|
? value
|
|
: undefined
|
|
}
|
|
|
|
export function readCheckRunStatus(value: unknown): PRCheckDetail['status'] | null {
|
|
return value === 'queued' || value === 'in_progress' || value === 'completed' ? value : null
|
|
}
|
|
|
|
// Why: dropping `action_required` here rendered a merge-blocking approval gate as a pending
|
|
// check; the shared classifier counts it as a failure, so it must survive parsing.
|
|
export function readCheckRunConclusion(value: unknown): PRCheckDetail['conclusion'] {
|
|
return value === 'success' ||
|
|
value === 'failure' ||
|
|
value === 'cancelled' ||
|
|
value === 'timed_out' ||
|
|
value === 'action_required' ||
|
|
value === 'neutral' ||
|
|
value === 'skipped' ||
|
|
value === 'pending'
|
|
? value
|
|
: null
|
|
}
|
|
|
|
export function readAssignableUser(value: unknown): GitHubAssignableUser | null {
|
|
if (!isRecord(value)) {
|
|
return null
|
|
}
|
|
const login = readString(value.login)
|
|
if (login === undefined) {
|
|
return null
|
|
}
|
|
return {
|
|
login,
|
|
name: readString(value.name) ?? null,
|
|
avatarUrl: readString(value.avatarUrl) ?? ''
|
|
}
|
|
}
|
|
|
|
export function readAssignableUserArray(value: unknown): GitHubAssignableUser[] {
|
|
if (!Array.isArray(value)) {
|
|
return []
|
|
}
|
|
return value.flatMap((entry): GitHubAssignableUser[] => {
|
|
const parsed = readAssignableUser(entry)
|
|
return parsed ? [parsed] : []
|
|
})
|
|
}
|
|
|
|
export function readReviewSummary(value: unknown): GitHubPRReviewSummary | null {
|
|
if (!isRecord(value)) {
|
|
return null
|
|
}
|
|
// Desktop maps latestReviews to top-level `login`. Raw `gh pr view --json`
|
|
// keeps nested `author.login` — accept both so mobile never drops reviewers.
|
|
const nestedAuthor = isRecord(value.author) ? value.author : null
|
|
const login =
|
|
readString(value.login) ?? (nestedAuthor ? readString(nestedAuthor.login) : undefined)
|
|
if (login === undefined) {
|
|
return null
|
|
}
|
|
const avatarUrl =
|
|
readString(value.avatarUrl) ??
|
|
(nestedAuthor
|
|
? (readString(nestedAuthor.avatarUrl) ?? readString(nestedAuthor.avatar_url) ?? null)
|
|
: null)
|
|
return {
|
|
login,
|
|
state: readString(value.state) ?? null,
|
|
avatarUrl
|
|
}
|
|
}
|
|
|
|
export function readRepoIdentity(value: unknown): GitHubRepositoryIdentity | undefined {
|
|
if (!isRecord(value)) {
|
|
return undefined
|
|
}
|
|
const owner = readString(value.owner)
|
|
const repo = readString(value.repo)
|
|
// Empty owner/repo is malformed, not a valid identity — drop it before it reaches prRepo parsing.
|
|
if (!owner || !repo) {
|
|
return undefined
|
|
}
|
|
// Why: dropping `host` here would strip the GHES identity before every
|
|
// subsequent PR RPC, forcing the host to re-derive it per call.
|
|
const host = readString(value.host)
|
|
return { owner, repo, ...(host ? { host } : {}) }
|
|
}
|
|
|
|
function readMergeMethod(value: unknown): GitHubPRMergeMethod | undefined {
|
|
return value === 'merge' || value === 'squash' || value === 'rebase' ? value : undefined
|
|
}
|
|
|
|
export function readMergeMethodSettings(value: unknown): GitHubPRMergeMethodSettings | undefined {
|
|
if (!isRecord(value)) {
|
|
return undefined
|
|
}
|
|
const defaultMethod = readMergeMethod(value.defaultMethod)
|
|
if (defaultMethod === undefined || !isRecord(value.allowedMethods)) {
|
|
return undefined
|
|
}
|
|
const allowed = value.allowedMethods
|
|
return {
|
|
defaultMethod,
|
|
allowedMethods: {
|
|
merge: readBoolean(allowed.merge) ?? false,
|
|
squash: readBoolean(allowed.squash) ?? false,
|
|
rebase: readBoolean(allowed.rebase) ?? false
|
|
}
|
|
}
|
|
}
|
|
|
|
export function readCheckSummary(value: unknown): ProviderCheckSummary | undefined {
|
|
if (!isRecord(value)) {
|
|
return undefined
|
|
}
|
|
const state = value.state
|
|
if (
|
|
state !== 'success' &&
|
|
state !== 'failure' &&
|
|
state !== 'pending' &&
|
|
state !== 'neutral' &&
|
|
state !== 'none'
|
|
) {
|
|
return undefined
|
|
}
|
|
return {
|
|
state,
|
|
total: readNumber(value.total) ?? 0,
|
|
passed: readNumber(value.passed) ?? 0,
|
|
failed: readNumber(value.failed) ?? 0,
|
|
pending: readNumber(value.pending) ?? 0,
|
|
neutral: readNumber(value.neutral) ?? 0
|
|
}
|
|
}
|