diff --git a/package.json b/package.json index 16e4667c95..6100654e78 100644 --- a/package.json +++ b/package.json @@ -52,9 +52,12 @@ "test:coverage": "pnpm run build:all && node --test --experimental-test-coverage --test-coverage-include='dist/**/*.js' --test-coverage-exclude='dist/repair/*.test.js' --test-coverage-lines=49 --test-coverage-branches=66 --test-coverage-functions=57 test/*.test.ts test/repair/*.test.ts dist/repair/*.test.js", "test:coverage:changed": "pnpm run build:all && node --test --experimental-test-coverage --test-coverage-include='dist/repair/fix-prompt-builder.js' --test-coverage-lines=85 --test-coverage-branches=85 --test-coverage-functions=85 test/repair/*.test.ts dist/repair/*.test.js", "check:active-surface": "node scripts/check-active-surface.ts", - "lint": "oxlint src --tsconfig tsconfig.json && oxlint src/repair --tsconfig tsconfig.repair.json", - "format": "oxfmt --write src scripts test package.json tsconfig.json tsconfig.repair.json .oxfmtrc.json schema .github/workflows", - "format:check": "oxfmt --check src scripts test package.json tsconfig.json tsconfig.repair.json .oxfmtrc.json schema .github/workflows", + "lint": "pnpm run lint:src && pnpm run lint:repair && pnpm run lint:scripts", + "lint:src": "oxlint src/*.ts --tsconfig tsconfig.json --type-aware --deny-warnings --report-unused-disable-directives -D correctness", + "lint:repair": "oxlint src/repair --tsconfig tsconfig.repair.json --deny-warnings --report-unused-disable-directives -D correctness", + "lint:scripts": "oxlint scripts test --deny-warnings --report-unused-disable-directives -D correctness", + "format": "oxfmt --write src scripts test package.json tsconfig.json tsconfig.repair.json .oxfmtrc.json schema .github/actions .github/workflows", + "format:check": "oxfmt --check src scripts test package.json tsconfig.json tsconfig.repair.json .oxfmtrc.json schema .github/actions .github/workflows", "oxformat": "pnpm run format", "oxformat:check": "pnpm run format:check", "check": "pnpm run check:active-surface && pnpm run build:all && pnpm run lint && pnpm run test:unit && pnpm run test:repair && pnpm run test:coverage:changed && pnpm run test:coverage && pnpm run format:check" @@ -63,7 +66,8 @@ "@types/node": "^25.6.0", "@typescript/native-preview": "7.0.0-dev.20260423.1", "oxfmt": "^0.46.0", - "oxlint": "^1.61.0" + "oxlint": "^1.61.0", + "oxlint-tsgolint": "0.22.1" }, "engines": { "node": ">=24" diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 039004263d..142b5c70ea 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -19,7 +19,10 @@ importers: version: 0.46.0 oxlint: specifier: ^1.61.0 - version: 1.61.0 + version: 1.61.0(oxlint-tsgolint@0.22.1) + oxlint-tsgolint: + specifier: 0.22.1 + version: 0.22.1 packages: @@ -145,6 +148,36 @@ packages: cpu: [x64] os: [win32] + '@oxlint-tsgolint/darwin-arm64@0.22.1': + resolution: {integrity: sha512-4150Lpgc1YM09GcjA6GSrra1JoPjC7aOpfywLjWEY4vW0Sd1qKzqHF1WRaiw0/qUZ40OATYdv3aRd7ipPkWQbw==} + cpu: [arm64] + os: [darwin] + + '@oxlint-tsgolint/darwin-x64@0.22.1': + resolution: {integrity: sha512-vFWcPWYOgZs4HWcgS1EjUZg33NLcNfEYU49KGImmCfZWkflENrmBYV4HN/C0YeAPum6ZZ/goPSvQrB/cOD+NfA==} + cpu: [x64] + os: [darwin] + + '@oxlint-tsgolint/linux-arm64@0.22.1': + resolution: {integrity: sha512-6LiUpP0Zir3+29FvBm7Y28q/dBjSHqTZ5MhG1Ckw4fGhI4cAvbcwXaKvbjx1TP7rRmBNOoq/M5xdpHjTb+GAew==} + cpu: [arm64] + os: [linux] + + '@oxlint-tsgolint/linux-x64@0.22.1': + resolution: {integrity: sha512-fuX1hEQfpHauUbXADsfqVhRzrUrGabzGXbj5wsp2vKhV5uk/Rze8Mba9GdjFGECzvXudMGqHqxB4r6jGRdhxVA==} + cpu: [x64] + os: [linux] + + '@oxlint-tsgolint/win32-arm64@0.22.1': + resolution: {integrity: sha512-8SZidAj+jrbZf9ZjBEYW0tiNZ+KasqB2zgW26qdiPpQSF/DzURnPmXz651IeA9YsmbVdHGIooEHUmev6QJdquA==} + cpu: [arm64] + os: [win32] + + '@oxlint-tsgolint/win32-x64@0.22.1': + resolution: {integrity: sha512-QweSk9H5lFh5Y+WUf2Kq/OAN88V6+62ZwGhP38gqdRotI90luXSMkruFTj7Q2rYrzH4ZVNaSqx7NY8JpSfIzqg==} + cpu: [x64] + os: [win32] + '@oxlint/binding-android-arm-eabi@1.61.0': resolution: {integrity: sha512-6eZBPgiigK5txqoVgRqxbaxiom4lM8AP8CyKPPvpzKnQ3iFRFOIDc+0AapF+qsUSwjOzr5SGk4SxQDpQhkSJMQ==} engines: {node: ^20.19.0 || >=22.12.0} @@ -322,6 +355,10 @@ packages: engines: {node: ^20.19.0 || >=22.12.0} hasBin: true + oxlint-tsgolint@0.22.1: + resolution: {integrity: sha512-YUSGSLUnoolsu8gxISEDio3q1rtsCozwfOzASUn3DT2mR2EeQ93uEEnen7s+6LpF+lyTQFln1pQfqwBh/fsVEg==} + hasBin: true + oxlint@1.61.0: resolution: {integrity: sha512-ZC0ALuhDZ6ivOFG+sy0D0pEDN49EvsId98zVlmYdkcXHsEM14m/qTNUEsUpiFiCVbpIxYtVBmmLE87nsbUHohQ==} engines: {node: ^20.19.0 || >=22.12.0} @@ -398,6 +435,24 @@ snapshots: '@oxfmt/binding-win32-x64-msvc@0.46.0': optional: true + '@oxlint-tsgolint/darwin-arm64@0.22.1': + optional: true + + '@oxlint-tsgolint/darwin-x64@0.22.1': + optional: true + + '@oxlint-tsgolint/linux-arm64@0.22.1': + optional: true + + '@oxlint-tsgolint/linux-x64@0.22.1': + optional: true + + '@oxlint-tsgolint/win32-arm64@0.22.1': + optional: true + + '@oxlint-tsgolint/win32-x64@0.22.1': + optional: true + '@oxlint/binding-android-arm-eabi@1.61.0': optional: true @@ -514,7 +569,16 @@ snapshots: '@oxfmt/binding-win32-ia32-msvc': 0.46.0 '@oxfmt/binding-win32-x64-msvc': 0.46.0 - oxlint@1.61.0: + oxlint-tsgolint@0.22.1: + optionalDependencies: + '@oxlint-tsgolint/darwin-arm64': 0.22.1 + '@oxlint-tsgolint/darwin-x64': 0.22.1 + '@oxlint-tsgolint/linux-arm64': 0.22.1 + '@oxlint-tsgolint/linux-x64': 0.22.1 + '@oxlint-tsgolint/win32-arm64': 0.22.1 + '@oxlint-tsgolint/win32-x64': 0.22.1 + + oxlint@1.61.0(oxlint-tsgolint@0.22.1): optionalDependencies: '@oxlint/binding-android-arm-eabi': 1.61.0 '@oxlint/binding-android-arm64': 1.61.0 @@ -535,6 +599,7 @@ snapshots: '@oxlint/binding-win32-arm64-msvc': 1.61.0 '@oxlint/binding-win32-ia32-msvc': 1.61.0 '@oxlint/binding-win32-x64-msvc': 1.61.0 + oxlint-tsgolint: 0.22.1 tinypool@2.1.0: {} diff --git a/src/clawsweeper.ts b/src/clawsweeper.ts index e354983067..ae126c7ca8 100644 --- a/src/clawsweeper.ts +++ b/src/clawsweeper.ts @@ -1436,7 +1436,7 @@ function closingPullRequestsForIssue(number: number): unknown[] { export function openClosingPullRequestApplyReason(pullRequests: readonly unknown[]): string | null { const openPulls = pullRequests .map(asRecord) - .filter((pull) => String(pull.state ?? "").toLowerCase() === "open") + .filter((pull) => typeof pull.state === "string" && pull.state.toLowerCase() === "open") .map((pull) => ({ number: typeof pull.number === "number" ? pull.number : null, title: typeof pull.title === "string" ? pull.title : "", @@ -1502,7 +1502,7 @@ function collectRelatedMentions(options: { return mentions; } -function compactRelatedItem(number: number, mentionedIn: string[]): unknown | null { +function compactRelatedItem(number: number, mentionedIn: string[]): Record | null { try { const issue = ghJson(["api", `repos/${targetRepo()}/issues/${number}`]); const issueRecord = asRecord(issue); @@ -1748,11 +1748,12 @@ function relatedCounterpartInfo(value: unknown): { const issue = asRecord(record.issue); const pullRequest = asRecord(record.pullRequest); const isPullRequest = Object.keys(pullRequest).length > 0; + const state = isPullRequest ? pullRequest.state : issue.state; return { number: typeof issue.number === "number" ? issue.number : null, kind: isPullRequest ? "pull_request" : "issue", author: normalizeAuthorLogin(isPullRequest ? pullRequest.author : issue.author), - state: String((isPullRequest ? pullRequest.state : issue.state) ?? "").toLowerCase(), + state: typeof state === "string" ? state.toLowerCase() : "", title: typeof issue.title === "string" ? issue.title : "", }; } @@ -2714,8 +2715,8 @@ function collectItemContext(item: Item): ItemContext { timeline: timeline.length, }, }; - let pullRequest: unknown | undefined; - let pullReviewComments: unknown[] | undefined; + let pullRequest: unknown = null; + let pullReviewComments: unknown[] | null = null; if (item.kind === "issue") { const closingPullRequests = closingPullRequestsForIssue(item.number); if (closingPullRequests.length > 0) { diff --git a/src/repair/automerge-status-timeline.ts b/src/repair/automerge-status-timeline.ts index 890036c0c5..24569be508 100644 --- a/src/repair/automerge-status-timeline.ts +++ b/src/repair/automerge-status-timeline.ts @@ -33,7 +33,7 @@ function existingTimelineRows(value: JsonValue): string[] { new RegExp(`${escapeRegExp(TIMELINE_START)}([\\s\\S]*?)${escapeRegExp(TIMELINE_END)}`), ); if (!match) return []; - return match[1] + return match[1]! .split(/\r?\n/) .map((line) => line.trimEnd()) .filter((line) => line.includes(EVENT_PREFIX)); diff --git a/src/repair/comment-router.ts b/src/repair/comment-router.ts index b6122b7cec..ea7d0139e5 100644 --- a/src/repair/comment-router.ts +++ b/src/repair/comment-router.ts @@ -2411,7 +2411,7 @@ async function mapLimit( while (next < items.length) { const index = next; next += 1; - results[index] = await mapper(items[index]); + results[index] = await mapper(items[index] as T); } }); await Promise.all(workers); diff --git a/src/repair/commit-finding-intake.ts b/src/repair/commit-finding-intake.ts index c04ecf65f7..23410e48e3 100644 --- a/src/repair/commit-finding-intake.ts +++ b/src/repair/commit-finding-intake.ts @@ -441,9 +441,9 @@ function findingKinds(markdown: string) { function likelyFilesFromReport(markdown: string) { const out: JsonValue[] = []; for (const match of markdown.matchAll(/^- File:\s*`?([^`\n]+?)`?\s*$/gim)) - out.push(match[1].trim()); + out.push(match[1]!.trim()); const changed = markdown.match(/^- Changed files:\s*(.+)$/im)?.[1] ?? ""; - for (const match of changed.matchAll(/`([^`]+)`/g)) out.push(match[1].trim()); + for (const match of changed.matchAll(/`([^`]+)`/g)) out.push(match[1]!.trim()); return unique( out.filter( (file: JsonValue) => diff --git a/src/repair/fix-prompt-builder.ts b/src/repair/fix-prompt-builder.ts index 9558fc664d..d9b53b92d0 100644 --- a/src/repair/fix-prompt-builder.ts +++ b/src/repair/fix-prompt-builder.ts @@ -155,7 +155,7 @@ function focusedFileExcerpt(content: string, tokens: string[]) { .map((token) => token.toLowerCase()) .filter((token) => token.length >= 4); for (let index = 0; index < lines.length; index += 1) { - const lower = lines[index].toLowerCase(); + const lower = lines[index]!.toLowerCase(); if (lowerTokens.some((token) => lower.includes(token))) { for ( let line = Math.max(0, index - 8); diff --git a/src/repair/git-publish.ts b/src/repair/git-publish.ts index 096e4259fb..b427bb817d 100644 --- a/src/repair/git-publish.ts +++ b/src/repair/git-publish.ts @@ -12,11 +12,11 @@ export type GitPublishOptions = { message: string; paths: readonly string[]; restorePaths?: readonly string[]; - maxAttempts?: number; - pushAttempts?: number; + maxAttempts?: number | undefined; + pushAttempts?: number | undefined; remote?: string; branch?: string; - rebaseStrategy?: RebaseStrategy; + rebaseStrategy?: RebaseStrategy | undefined; }; export type RebaseStrategy = "normal" | "theirs" | "apply-records"; diff --git a/src/repair/git-repo-utils.ts b/src/repair/git-repo-utils.ts index a665c65db8..5fac8d9983 100644 --- a/src/repair/git-repo-utils.ts +++ b/src/repair/git-repo-utils.ts @@ -60,8 +60,8 @@ export function remoteBranchSha({ targetDir, branch }: TargetBranch): string { encoding: "utf8", }); if (child.status !== 0) return ""; - const [sha] = child.stdout.trim().split(/\s+/); - return /^[0-9a-f]{40}$/.test(sha ?? "") ? sha : ""; + const sha = child.stdout.trim().split(/\s+/)[0] ?? ""; + return /^[0-9a-f]{40}$/.test(sha) ? sha : ""; } export function branchHasBaseDiff({ targetDir, baseBranch }: TargetBaseBranch): boolean { diff --git a/src/repair/github-ref.ts b/src/repair/github-ref.ts index 7406034c2c..a9da3987cf 100644 --- a/src/repair/github-ref.ts +++ b/src/repair/github-ref.ts @@ -9,10 +9,12 @@ export function parsePullRequestUrl(value: unknown): GitHubRef | null { .trim() .match(/^https:\/\/github\.com\/([^/\s]+\/[^/\s]+)\/pull\/(\d+)(?:[/?#].*)?$/i); if (!match) return null; + const repo = match[1]!; + const numberText = match[2]!; return { - repo: match[1], - number: Number(match[2]), - url: `https://github.com/${match[1]}/pull/${match[2]}`, + repo, + number: Number(numberText), + url: `https://github.com/${repo}/pull/${numberText}`, }; } @@ -26,19 +28,22 @@ export function parseIssueOrPullRef(value: unknown, defaultRepo = ""): GitHubRef /^https:\/\/github\.com\/([^/]+\/[^/]+)\/(?:issues|pull)\/(\d+)(?:[/?#].*)?$/i, ); if (urlMatch) { + const repo = urlMatch[1]!; + const numberText = urlMatch[2]!; return { - repo: urlMatch[1], - number: Number(urlMatch[2]), - url: `https://github.com/${urlMatch[1]}/issues/${urlMatch[2]}`, + repo, + number: Number(numberText), + url: `https://github.com/${repo}/issues/${numberText}`, }; } const shorthand = text.match(/^#?(\d+)$/); if (!shorthand || !defaultRepo) return null; + const numberText = shorthand[1]!; return { repo: defaultRepo, - number: Number(shorthand[1]), - url: `https://github.com/${defaultRepo}/issues/${shorthand[1]}`, + number: Number(numberText), + url: `https://github.com/${defaultRepo}/issues/${numberText}`, }; } @@ -46,7 +51,7 @@ export function issueNumberFromRef(value: unknown, expectedRepo = ""): number { const shorthand = String(value ?? "") .trim() .match(/^#?(\d+)$/); - if (shorthand) return Number(shorthand[1]); + if (shorthand) return Number(shorthand[1]!); const parsed = parseIssueOrPullRef(value); if (!parsed) return 0; diff --git a/src/repair/pr-title.ts b/src/repair/pr-title.ts index 85205d916f..8397276590 100644 --- a/src/repair/pr-title.ts +++ b/src/repair/pr-title.ts @@ -77,10 +77,8 @@ function repairStem(surface: unknown, issue: unknown) { } function firstSentence(value: unknown) { - return normalizeTitleText(value) - .split(/(?<=[.!?])\s+/)[0] - .replace(/[.!?]+$/, "") - .trim(); + const first = normalizeTitleText(value).split(/(?<=[.!?])\s+/)[0]!; + return first.replace(/[.!?]+$/, "").trim(); } function compactTitle(value: unknown, maxLength: number) { diff --git a/src/repair/target-validation.ts b/src/repair/target-validation.ts index c402872e9b..7f2227bad4 100644 --- a/src/repair/target-validation.ts +++ b/src/repair/target-validation.ts @@ -97,11 +97,12 @@ export function runAllowedValidationCommands( for (const command of requiredValidationCommands(commands, cwd, options)) { const resolvedCommands = resolveAllowedValidationCommands(command, cwd, baseBranch, options); for (const parts of resolvedCommands) { + const executable = parts[0]!; const rendered = parts.join(" "); if (executed.includes(rendered)) continue; while (true) { try { - run(parts[0], parts.slice(1), { cwd, env: validationEnv }); + run(executable, parts.slice(1), { cwd, env: validationEnv }); executed.push(rendered); break; } catch (error) { @@ -114,9 +115,10 @@ export function runAllowedValidationCommands( }); if (fallbackCommands.length > 0) { for (const fallbackParts of fallbackCommands) { + const fallbackExecutable = fallbackParts[0]!; const fallbackRendered = fallbackParts.join(" "); if (executed.includes(fallbackRendered)) continue; - run(fallbackParts[0], fallbackParts.slice(1), { cwd, env: validationEnv }); + run(fallbackExecutable, fallbackParts.slice(1), { cwd, env: validationEnv }); executed.push(fallbackRendered); } break; diff --git a/tsconfig.repair.json b/tsconfig.repair.json index c788fec0a4..0e2a42bc76 100644 --- a/tsconfig.repair.json +++ b/tsconfig.repair.json @@ -8,6 +8,8 @@ "rootDir": "src/repair", "outDir": "dist/repair", "strict": true, + "noUncheckedIndexedAccess": true, + "exactOptionalPropertyTypes": true, "useUnknownInCatchVariables": false, "esModuleInterop": true, "forceConsistentCasingInFileNames": true,