From 46eb3cc1533ac1744f6d203959021977c13bf7a7 Mon Sep 17 00:00:00 2001 From: Hasan TASKIN Date: Sat, 18 Jul 2026 06:59:33 +0200 Subject: [PATCH 01/70] chore: add oxlint hardened config and fix violations --- .oxlintrc.json | 62 +++++++++++++ bun.lock | 45 ++++++++- package.json | 4 +- packages/cli/eval/run.ts | 2 +- packages/cli/src/agent.ts | 40 ++++---- packages/cli/src/branches.ts | 4 +- packages/cli/src/config.test.ts | 12 +-- packages/cli/src/config.ts | 10 +- packages/cli/src/dual.ts | 46 +++++----- packages/cli/src/export.ts | 26 +++--- packages/cli/src/fix.test.ts | 6 +- packages/cli/src/fix.ts | 29 +++--- packages/cli/src/git.ts | 4 +- packages/cli/src/i18n.ts | 2 +- packages/cli/src/impact.ts | 42 ++++----- packages/cli/src/index.ts | 6 +- packages/cli/src/menu.ts | 4 +- packages/cli/src/notify.ts | 2 +- packages/cli/src/partial.test.ts | 2 +- packages/cli/src/partial.ts | 40 ++++---- packages/cli/src/prep.ts | 26 +++--- packages/cli/src/record.test.ts | 2 +- packages/cli/src/record.ts | 24 +++-- packages/cli/src/review.test.ts | 6 +- packages/cli/src/review.ts | 92 +++++++++---------- packages/cli/src/serve.test.ts | 4 +- packages/cli/src/serve.ts | 46 +++++----- packages/cli/src/show.ts | 2 +- packages/cli/src/summary.ts | 8 +- packages/cli/src/sync-commands.test.ts | 8 +- packages/cli/src/sync.test.ts | 8 +- packages/cli/src/sync.ts | 34 +++---- packages/cli/src/tui.ts | 26 +++--- packages/cli/src/ui.test.ts | 5 +- packages/cli/src/ui.ts | 24 +++-- packages/cli/src/version.ts | 10 +- packages/cli/src/wizard.ts | 46 +++++----- packages/contract/src/index.test.ts | 10 +- packages/contract/src/index.ts | 88 +++++++++--------- packages/web/src/App.vue | 6 +- packages/web/src/components/DiffView.vue | 28 +++--- .../web/src/components/DualJudgePanel.vue | 4 +- packages/web/src/components/DualLaneCard.vue | 4 +- packages/web/src/components/FileTree.vue | 17 ++-- packages/web/src/components/FileTreeNode.vue | 4 +- packages/web/src/components/FocusView.vue | 36 ++++---- packages/web/src/components/ReviewLive.vue | 4 +- .../web/src/components/ReviewPrologue.vue | 7 +- packages/web/src/components/ReviewShell.vue | 37 ++++---- packages/web/src/components/StepList.vue | 4 +- packages/web/src/components/StepRail.vue | 6 +- packages/web/src/components/StepReview.vue | 14 +-- .../src/composables/useConsensusTree.test.ts | 2 +- .../web/src/composables/useConsensusTree.ts | 5 +- packages/web/src/composables/useDiff.ts | 39 ++++---- packages/web/src/composables/useFixPrompt.ts | 2 +- packages/web/src/composables/useFocusList.ts | 18 ++-- packages/web/src/composables/useNoteTour.ts | 2 +- .../web/src/composables/useReviewProgress.ts | 8 +- packages/web/src/composables/useStepTone.ts | 12 +-- packages/web/src/i18n.ts | 4 +- packages/web/tsconfig.json | 2 +- tsconfig.base.json | 2 +- 63 files changed, 623 insertions(+), 501 deletions(-) create mode 100644 .oxlintrc.json diff --git a/.oxlintrc.json b/.oxlintrc.json new file mode 100644 index 0000000..1fef658 --- /dev/null +++ b/.oxlintrc.json @@ -0,0 +1,62 @@ +{ + "$schema": "./node_modules/oxlint/configuration_schema.json", + "categories": { + "correctness": "error", + "perf": "error", + "suspicious": "error" + }, + "rules": { + "complexity": ["warn", 10], + "curly": ["error", "all"], + "eqeqeq": ["error", "always", { "null": "ignore" }], + "max-depth": ["warn", 3], + "max-lines": ["warn", { "max": 400, "skipBlankLines": true, "skipComments": true }], + "max-lines-per-function": ["warn", { "max": 60, "skipBlankLines": true, "skipComments": true }], + "max-nested-callbacks": ["warn", 3], + "max-params": ["error", { "max": 4 }], + "no-await-in-loop": "off", + "no-else-return": ["error", { "allowElseIf": false }], + "no-lonely-if": "error", + "no-param-reassign": "error", + "no-underscore-dangle": ["error", { "allow": ["__CODESEMA_VERSION__", "__CODESEMA_LOCALE__", "__CODESEMA_FIX_TOKEN__"] }], + "oxc/no-map-spread": "off", + "typescript/consistent-type-definitions": ["error", "type"], + "typescript/no-explicit-any": "error", + "typescript/no-non-null-assertion": "error", + "typescript/no-require-imports": "error", + "unicorn/consistent-function-scoping": "warn", + "unicorn/filename-case": ["error", { "case": "kebabCase" }], + "unicorn/prefer-node-protocol": "error" + }, + "overrides": [ + { + "files": ["**/*.test.ts"], + "rules": { + "max-lines": "off", + "max-lines-per-function": "off", + "max-nested-callbacks": ["warn", 5], + "typescript/no-non-null-assertion": "off" + } + }, + { + "files": ["**/*.vue"], + "rules": { + "unicorn/filename-case": ["error", { "case": "pascalCase" }] + } + }, + { + "files": ["packages/web/src/composables/**"], + "rules": { + "unicorn/filename-case": ["error", { "case": "camelCase" }] + } + }, + { + "files": ["**/*.d.ts"], + "rules": { + "typescript/consistent-type-definitions": "off", + "unicorn/require-module-specifiers": "off" + } + } + ], + "ignorePatterns": ["node_modules", "dist", "web-dist", "coverage"] +} diff --git a/bun.lock b/bun.lock index 1e50e0c..f3220fd 100644 --- a/bun.lock +++ b/bun.lock @@ -6,11 +6,12 @@ "name": "codesema-monorepo", "devDependencies": { "@types/bun": "^1.3.14", + "oxlint": "1.74.0", }, }, "packages/cli": { "name": "codesema", - "version": "0.5.0", + "version": "0.8.0", "bin": { "codesema": "dist/index.mjs", }, @@ -23,7 +24,7 @@ }, "packages/contract": { "name": "@codesema/contract", - "version": "0.1.0", + "version": "0.3.0", "devDependencies": { "@types/node": "^26.1.1", "tsdown": "^0.22.5", @@ -69,6 +70,44 @@ "@oxc-project/types": ["@oxc-project/types@0.139.0", "", {}, "sha512-r9gHphtCs+1M7J0pw6Sn/hh/Wpa/iQrOOkrNAlVLF/gHq+/CJmHIWKKUUhdWjcD6CIa8idarspCsASiXCXvFUw=="], + "@oxlint/binding-android-arm-eabi": ["@oxlint/binding-android-arm-eabi@1.74.0", "", { "os": "android", "cpu": "arm" }, "sha512-+gHd12muVI9ZLBaWLPkHt3Fj7jihFjgQ1MGtBaRL8vWrWrI0P7dLUty/cHrHS0oqPYIRgQUJsPu2CExQuMcwNw=="], + + "@oxlint/binding-android-arm64": ["@oxlint/binding-android-arm64@1.74.0", "", { "os": "android", "cpu": "arm64" }, "sha512-xjKdoMB+H+RCOByv/7l7nfIGW9mlOisqYdcyC75UqYuQecLpReAeEYUf2CNeDEI3KtmUgxpRw/+c63y4AeF/Bw=="], + + "@oxlint/binding-darwin-arm64": ["@oxlint/binding-darwin-arm64@1.74.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-iUK7wvc6sejMKsC+Pt67mntoF5weFcyEunhZfLJceU6gL419mexz5wBkSx/EnkFBExMLNtOi9fnDSc5xfK0IzQ=="], + + "@oxlint/binding-darwin-x64": ["@oxlint/binding-darwin-x64@1.74.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-ggKc/tn5SJ1u2yG2izC6VKODfYKV8MQ2AicJlNzOjuyrC29udvOef6/JzK2r32xqCnBDLFouR1VCkjzEI0/N9Q=="], + + "@oxlint/binding-freebsd-x64": ["@oxlint/binding-freebsd-x64@1.74.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-u++dH/43jy9hTLbneaWlS0gla/Bp1JdwJ2zgevCl8nDFUh6qRCGMxcL0f0lb7By3A9p/LfFr+7cG4HU1hG856g=="], + + "@oxlint/binding-linux-arm-gnueabihf": ["@oxlint/binding-linux-arm-gnueabihf@1.74.0", "", { "os": "linux", "cpu": "arm" }, "sha512-Sj1zmtFDVTPeIbIz4ZfcXAbFHqCmKCXdCUlAJzvTF7I20NTH1RDpoF2PhkqNODutJzVhJYmm3oz0GwgY+tvE2g=="], + + "@oxlint/binding-linux-arm-musleabihf": ["@oxlint/binding-linux-arm-musleabihf@1.74.0", "", { "os": "linux", "cpu": "arm" }, "sha512-//PKyQb/tQXcHArx2f7z+oVI/eMS2Jpv+edNuAtOrgIhWdGcpHxogveAxzmF2rpH1AIHp4Hq04RF/rgJdiICnQ=="], + + "@oxlint/binding-linux-arm64-gnu": ["@oxlint/binding-linux-arm64-gnu@1.74.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-/k1Me+aX2tjuH10K62mLS0y8cLkJBHX6Ce0xPK+eWeel4bSdEGZ8dv4+hYMzg0GrSmjwy4yAYsDPeEeKBft/2w=="], + + "@oxlint/binding-linux-arm64-musl": ["@oxlint/binding-linux-arm64-musl@1.74.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-3tFSjBxc5D8/zvjEuLvOqcA8ZXKD0+6NuaVO/edeamNc49MoAsbfaC9s1UiwODwgF6slGaF8yJA2TPkukd77tg=="], + + "@oxlint/binding-linux-ppc64-gnu": ["@oxlint/binding-linux-ppc64-gnu@1.74.0", "", { "os": "linux", "cpu": "ppc64" }, "sha512-9QggtPkSPXOCTu8Szis7auOK/sC7KdQaN+/TujP7YVVhzCAOhgdRfgv8uEz0r2tk5xdgus5rLYUrCDoZNtiRUw=="], + + "@oxlint/binding-linux-riscv64-gnu": ["@oxlint/binding-linux-riscv64-gnu@1.74.0", "", { "os": "linux", "cpu": "none" }, "sha512-VM5VPUJ4DJIWiK+AZn8FScUqMr6OFrCAYybMYjEEi7W13ParI64MByiXTkKMqZpBmvQ9zxl9Ebq2VUOiZRJYUg=="], + + "@oxlint/binding-linux-riscv64-musl": ["@oxlint/binding-linux-riscv64-musl@1.74.0", "", { "os": "linux", "cpu": "none" }, "sha512-SaDY1gh9rOA592J54g+gu5hkOFFQBZsMmIYHs+NRHG+Uq0OxtuuCXMWQ3vu1830Eugv5uMXyjG+bv2Z9y4IXjw=="], + + "@oxlint/binding-linux-s390x-gnu": ["@oxlint/binding-linux-s390x-gnu@1.74.0", "", { "os": "linux", "cpu": "s390x" }, "sha512-ZATQeHZCyr6MbDveg0obD5sxLHFOghtOdC5jwVwYlvFWqtFOxctgFEG6Ef/64hYvZrWyhyCckB10AelqLopeDA=="], + + "@oxlint/binding-linux-x64-gnu": ["@oxlint/binding-linux-x64-gnu@1.74.0", "", { "os": "linux", "cpu": "x64" }, "sha512-+aIvJyrdeD7LwCQ2WYLMUWNmnbeDRSPb40aBYtPjD9+PTqUwgJnk+HK5yLfSMeqXrMrDhE9uTmtt2y50tvjhHw=="], + + "@oxlint/binding-linux-x64-musl": ["@oxlint/binding-linux-x64-musl@1.74.0", "", { "os": "linux", "cpu": "x64" }, "sha512-XyktaR8lhK2qWiCK0Tk8oYD+/cgn+oHA6ddRnxSSXUKkkojkV78CmShZUxQF+yrBFs0SuW+JBOPG6hecyc/iZg=="], + + "@oxlint/binding-openharmony-arm64": ["@oxlint/binding-openharmony-arm64@1.74.0", "", { "os": "none", "cpu": "arm64" }, "sha512-mzbjrPl4neaVUiJ1fUiEUxTGaSZBoiKtaoB6jmIpz9S+VOA2vDYmJpihQ82w6178V5jxziclTg8Cgj5yF6tTDg=="], + + "@oxlint/binding-win32-arm64-msvc": ["@oxlint/binding-win32-arm64-msvc@1.74.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-vUAe9okpS2Oa5+lX67lqHMuNUvfkleRKwrUDJ/WJBsgmddvZ1mrsh2HVmuFDRzqFELhaJhFaCNOuR6a7L3rtIA=="], + + "@oxlint/binding-win32-ia32-msvc": ["@oxlint/binding-win32-ia32-msvc@1.74.0", "", { "os": "win32", "cpu": "ia32" }, "sha512-yyXXJyYYSXL4I8K8jAWjJs+J3fa9gH2JmEbo4f5adm+1tNC9itseicBNuwK7BDHvqQ5J534s+yDULu89vYL2ZQ=="], + + "@oxlint/binding-win32-x64-msvc": ["@oxlint/binding-win32-x64-msvc@1.74.0", "", { "os": "win32", "cpu": "x64" }, "sha512-VTC9IYTIMrVUk/i6Ms1ohzzDKZFkWn0KU2OBbPBzgmVZ2V30165T/zK4LztTr0Xgp9fZ1qQZ1rsZAu/rEmySlA=="], + "@quansync/fs": ["@quansync/fs@1.0.0", "", { "dependencies": { "quansync": "^1.0.0" } }, "sha512-4TJ3DFtlf1L5LDMaM6CanJ/0lckGNtJcMjQ1NAV6zDmA0tEHKZtxNKin8EgPaVX1YzljbxckyT2tJrpQKAtngQ=="], "@rolldown/binding-android-arm64": ["@rolldown/binding-android-arm64@1.1.5", "", { "os": "android", "cpu": "arm64" }, "sha512-lZg8fqIv2v7FF237bwMgzGZEJvGL79/s5knJ/i6FmsGF4XXlzccZ4jb+TrFIxtSSxFtIpdsgrPZeMk1I9AFcyQ=="], @@ -249,6 +288,8 @@ "obug": ["obug@2.1.3", "", {}, "sha512-9miFgM2OFba7hB+pRgvtV84pYTBaoTHohvmIgiRt6dRIzbwEOIaNaP+dIlGs2fNFoB0SeISs0Jz5WFVRid6Xyg=="], + "oxlint": ["oxlint@1.74.0", "", { "optionalDependencies": { "@oxlint/binding-android-arm-eabi": "1.74.0", "@oxlint/binding-android-arm64": "1.74.0", "@oxlint/binding-darwin-arm64": "1.74.0", "@oxlint/binding-darwin-x64": "1.74.0", "@oxlint/binding-freebsd-x64": "1.74.0", "@oxlint/binding-linux-arm-gnueabihf": "1.74.0", "@oxlint/binding-linux-arm-musleabihf": "1.74.0", "@oxlint/binding-linux-arm64-gnu": "1.74.0", "@oxlint/binding-linux-arm64-musl": "1.74.0", "@oxlint/binding-linux-ppc64-gnu": "1.74.0", "@oxlint/binding-linux-riscv64-gnu": "1.74.0", "@oxlint/binding-linux-riscv64-musl": "1.74.0", "@oxlint/binding-linux-s390x-gnu": "1.74.0", "@oxlint/binding-linux-x64-gnu": "1.74.0", "@oxlint/binding-linux-x64-musl": "1.74.0", "@oxlint/binding-openharmony-arm64": "1.74.0", "@oxlint/binding-win32-arm64-msvc": "1.74.0", "@oxlint/binding-win32-ia32-msvc": "1.74.0", "@oxlint/binding-win32-x64-msvc": "1.74.0" }, "peerDependencies": { "oxlint-tsgolint": ">=0.24.0", "vite-plus": "*" }, "optionalPeers": ["oxlint-tsgolint", "vite-plus"], "bin": { "oxlint": "bin/oxlint" } }, "sha512-odGl2s2x5IOJoj3A0v1k0PGBXVFBZeZ2+AK/+K2MJur7Ghi3bkyX5NuLUWHKqa4js1wjep3hJeuTQJOlr+4+dA=="], + "path-browserify": ["path-browserify@1.0.1", "", {}, "sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g=="], "picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="], diff --git a/package.json b/package.json index 3d7d108..f5b5b45 100644 --- a/package.json +++ b/package.json @@ -11,6 +11,7 @@ "build:contract": "bun run --cwd packages/contract build", "build:web": "bun run --cwd packages/web build", "build:cli": "bun run --cwd packages/cli build", + "lint": "oxlint", "typecheck": "bun run build:contract && bun run --cwd packages/contract typecheck && bun run --cwd packages/cli typecheck && bun run --cwd packages/web typecheck", "test": "bun run build:contract && bun test packages/cli packages/web packages/contract" }, @@ -18,6 +19,7 @@ "node": ">=20" }, "devDependencies": { - "@types/bun": "^1.3.14" + "@types/bun": "^1.3.14", + "oxlint": "1.74.0" } } diff --git a/packages/cli/eval/run.ts b/packages/cli/eval/run.ts index 03d9afe..c574c12 100644 --- a/packages/cli/eval/run.ts +++ b/packages/cli/eval/run.ts @@ -59,7 +59,7 @@ const lanes: Lane[] = values.lane === 'both' ? ['a', 'b'] : [values.lane] const fixturesDir = join(import.meta.dir, 'fixtures') const fixtures = readdirSync(fixturesDir) .filter((name) => name.endsWith('.json')) - .sort() + .toSorted() .map((name) => JSON.parse(readFileSync(join(fixturesDir, name), 'utf8')) as Fixture) let totalExpected = 0 diff --git a/packages/cli/src/agent.ts b/packages/cli/src/agent.ts index 6263b92..d221832 100644 --- a/packages/cli/src/agent.ts +++ b/packages/cli/src/agent.ts @@ -39,9 +39,9 @@ export function hardenedReviewCommand(command: string): string { const agent = knownAgent(command) if (agent === 'claude') { const flags: string[] = [] - if (!flagPresent(command, '--tools')) flags.push('--tools ""') - if (!flagPresent(command, '--strict-mcp-config')) flags.push('--strict-mcp-config') - if (!flagPresent(command, '--setting-sources')) flags.push('--setting-sources user') + if (!flagPresent(command, '--tools')) {flags.push('--tools ""')} + if (!flagPresent(command, '--strict-mcp-config')) {flags.push('--strict-mcp-config')} + if (!flagPresent(command, '--setting-sources')) {flags.push('--setting-sources user')} return flags.length > 0 ? `${command} ${flags.join(' ')}` : command } if (agent === 'codex') { @@ -49,12 +49,12 @@ export function hardenedReviewCommand(command: string): string { return command } const flags: string[] = [] - if (!flagPresent(command, '--sandbox') && !flagPresent(command, '-s')) flags.push('--sandbox read-only') + if (!flagPresent(command, '--sandbox') && !flagPresent(command, '-s')) {flags.push('--sandbox read-only')} if (!flagPresent(command, '--ask-for-approval') && !flagPresent(command, '-a')) { flags.push('--ask-for-approval never') } - if (!flagPresent(command, 'project_doc_max_bytes')) flags.push('-c project_doc_max_bytes=0') - if (flags.length === 0) return command + if (!flagPresent(command, 'project_doc_max_bytes')) {flags.push('-c project_doc_max_bytes=0')} + if (flags.length === 0) {return command} const stdinMarker = /\s-$/.test(command) const base = stdinMarker ? command.slice(0, -2) : command return [base, ...flags, ...(stdinMarker ? ['-'] : [])].join(' ') @@ -112,15 +112,15 @@ export function agentEnv( ): NodeJS.ProcessEnv | undefined { // cmd.exe needs SystemRoot/ComSpec and Windows env names are case-insensitive: // narrowing there can break the spawn itself, so Windows inherits the full env. - if (platform === 'win32') return undefined + if (platform === 'win32') {return undefined} const agent = knownAgent(command) - if (!agent) return undefined + if (!agent) {return undefined} const prefixes = [...AGENT_ENV_PREFIXES[agent]] const names = new Set(BASE_ENV_VARS) // Claude Code on Bedrock/Vertex authenticates through the cloud SDK env, // not ANTHROPIC_*: widen only when those modes are switched on. if (agent === 'claude') { - if (source.CLAUDE_CODE_USE_BEDROCK) prefixes.push('AWS_') + if (source.CLAUDE_CODE_USE_BEDROCK) {prefixes.push('AWS_')} if (source.CLAUDE_CODE_USE_VERTEX) { prefixes.push('GOOGLE_', 'GCP_') names.add('CLOUD_ML_REGION') @@ -128,16 +128,16 @@ export function agentEnv( } const env: NodeJS.ProcessEnv = {} for (const [key, value] of Object.entries(source)) { - if (value === undefined) continue - if (names.has(key) || prefixes.some((prefix) => key.startsWith(prefix))) env[key] = value + if (value === undefined) {continue} + if (names.has(key) || prefixes.some((prefix) => key.startsWith(prefix))) {env[key] = value} } return env } export function claudeStreamCommand(command: string): string | null { - if (!/^claude(\s|$)/.test(command)) return null - if (!/(^|\s)(-p|--print)(\s|$)/.test(command)) return null - if (command.includes('--output-format') || command.includes('--input-format')) return null + if (!/^claude(\s|$)/.test(command)) {return null} + if (!/(^|\s)(-p|--print)(\s|$)/.test(command)) {return null} + if (command.includes('--output-format') || command.includes('--input-format')) {return null} return `${command} ${CLAUDE_STREAM_FLAGS}` } @@ -153,7 +153,7 @@ export function createClaudeStreamParser(onText?: (text: string) => void): Claud let resultText: string | null = null const handleLine = (line: string) => { - if (!line.trim()) return + if (!line.trim()) {return} let event: Record try { event = JSON.parse(line) as Record @@ -190,7 +190,7 @@ export function createClaudeStreamParser(onText?: (text: string) => void): Claud lineBuffer += chunk for (;;) { const nl = lineBuffer.indexOf('\n') - if (nl < 0) break + if (nl < 0) {break} handleLine(lineBuffer.slice(0, nl)) lineBuffer = lineBuffer.slice(nl + 1) } @@ -237,8 +237,8 @@ export function runAgent(opts: AgentRunOptions): Promise { const timer = setTimeout(() => { timedOut = true try { - if (detached && child.pid) process.kill(-child.pid, 'SIGTERM') - else child.kill('SIGTERM') + if (detached && child.pid) {process.kill(-child.pid, 'SIGTERM')} + else {child.kill('SIGTERM')} } catch { // process group already gone } @@ -247,8 +247,8 @@ export function runAgent(opts: AgentRunOptions): Promise { child.stdout.on('data', (d: Buffer) => { const chunk = d.toString() out += chunk - if (parser) parser.push(chunk) - else opts.onText?.(out) + if (parser) {parser.push(chunk)} + else {opts.onText?.(out)} }) child.on('error', (err) => { clearTimeout(timer) diff --git a/packages/cli/src/branches.ts b/packages/cli/src/branches.ts index 30b9475..fb55898 100644 --- a/packages/cli/src/branches.ts +++ b/packages/cli/src/branches.ts @@ -14,7 +14,7 @@ export function listLocalBranches(cwd: string): LocalBranch[] { ['for-each-ref', 'refs/heads', '--sort=-committerdate', '--format=%(refname:short)%09%(committerdate:relative)%09%(subject)'], cwd, ) - if (!out) return [] + if (!out) {return []} const current = tryGit(['rev-parse', '--abbrev-ref', 'HEAD'], cwd) return out .split('\n') @@ -34,7 +34,7 @@ export function listLocalBranches(cwd: string): LocalBranch[] { /** Interactive branch picker (keyboard filter). Returns null if cancelled, the current branch if non-TTY or the list is empty. */ export async function pickBranch(cwd: string): Promise { const branches = listLocalBranches(cwd) - if (branches.length <= 1) return branches[0]?.name ?? currentBranch(cwd) + if (branches.length <= 1) {return branches[0]?.name ?? currentBranch(cwd)} const initialIndex = Math.max(0, branches.findIndex((b) => b.isCurrent)) const picked = await select({ diff --git a/packages/cli/src/config.test.ts b/packages/cli/src/config.test.ts index 9567988..ba489d0 100644 --- a/packages/cli/src/config.test.ts +++ b/packages/cli/src/config.test.ts @@ -25,8 +25,8 @@ describe('repo agent trust store', () => { }) afterEach(() => { - if (previousConfigDir === undefined) delete process.env.CODESEMA_CONFIG_DIR - else process.env.CODESEMA_CONFIG_DIR = previousConfigDir + if (previousConfigDir === undefined) {delete process.env.CODESEMA_CONFIG_DIR} + else {process.env.CODESEMA_CONFIG_DIR = previousConfigDir} rmSync(configDir, { recursive: true, force: true }) }) @@ -64,8 +64,8 @@ describe('sync credentials round-trip', () => { }) afterEach(() => { - if (previousConfigDir === undefined) delete process.env.CODESEMA_CONFIG_DIR - else process.env.CODESEMA_CONFIG_DIR = previousConfigDir + if (previousConfigDir === undefined) {delete process.env.CODESEMA_CONFIG_DIR} + else {process.env.CODESEMA_CONFIG_DIR = previousConfigDir} rmSync(configDir, { recursive: true, force: true }) }) @@ -109,8 +109,8 @@ describe('sync fields are global-only', () => { }) afterEach(() => { - if (previousConfigDir === undefined) delete process.env.CODESEMA_CONFIG_DIR - else process.env.CODESEMA_CONFIG_DIR = previousConfigDir + if (previousConfigDir === undefined) {delete process.env.CODESEMA_CONFIG_DIR} + else {process.env.CODESEMA_CONFIG_DIR = previousConfigDir} rmSync(configDir, { recursive: true, force: true }) rmSync(repoDir, { recursive: true, force: true }) }) diff --git a/packages/cli/src/config.ts b/packages/cli/src/config.ts index ef1f74d..c80ddbe 100644 --- a/packages/cli/src/config.ts +++ b/packages/cli/src/config.ts @@ -26,7 +26,7 @@ export type CodesemaConfig = { type ConfigScope = 'global' | 'repo' function parseConfig(path: string, scope: ConfigScope): CodesemaConfig { - if (!existsSync(path)) return {} + if (!existsSync(path)) {return {}} try { const raw = JSON.parse(readFileSync(path, 'utf8')) as Record const str = (v: unknown) => (typeof v === 'string' && v ? v : undefined) @@ -57,7 +57,7 @@ function writeConfig(path: string, config: CodesemaConfig, options?: { mode: num } export function globalConfigDir(): string { - if (process.env.CODESEMA_CONFIG_DIR) return process.env.CODESEMA_CONFIG_DIR + if (process.env.CODESEMA_CONFIG_DIR) {return process.env.CODESEMA_CONFIG_DIR} const base = process.env.XDG_CONFIG_HOME || join(homedir(), '.config') return join(base, 'codesema') } @@ -109,12 +109,12 @@ export function trustStorePath(): string { function readTrustStore(): Record { const path = trustStorePath() - if (!existsSync(path)) return {} + if (!existsSync(path)) {return {}} try { const raw = JSON.parse(readFileSync(path, 'utf8')) as Record const out: Record = {} for (const [key, value] of Object.entries(raw)) { - if (typeof value === 'string') out[key] = value + if (typeof value === 'string') {out[key] = value} } return out } catch { @@ -140,6 +140,6 @@ export function ensureWorkDir(repoRoot: string): string { const dir = join(repoRoot, '.codesema') mkdirSync(dir, { recursive: true }) const selfIgnore = join(dir, '.gitignore') - if (!existsSync(selfIgnore)) writeFileSync(selfIgnore, '*\n') + if (!existsSync(selfIgnore)) {writeFileSync(selfIgnore, '*\n')} return dir } diff --git a/packages/cli/src/dual.ts b/packages/cli/src/dual.ts index 73c3348..9959ff5 100644 --- a/packages/cli/src/dual.ts +++ b/packages/cli/src/dual.ts @@ -2,7 +2,7 @@ import type { DualStats, Finding, FindingSeverity, SanitizedReview, Verdict } fr import { repairTruncatedJson } from './partial.js' import { AGENT_DEFS } from './wizard.js' -const SEVERITIES: readonly FindingSeverity[] = ['critical', 'major', 'minor', 'info'] +const SEVERITIES: ReadonlySet = new Set(['critical', 'major', 'minor', 'info']) const SEVERITY_ORDER: Record = { info: 0, minor: 1, major: 2, critical: 3 } const VERDICT_ORDER: Record = { approve: 0, comment: 1, request_changes: 2 } const JUDGE_REASON_MAX = 300 @@ -24,7 +24,7 @@ export function judgeCommandFor(command: string): string { const first = command.trim().split(/\s+/)[0] ?? '' const bin = first.split('/').pop() ?? '' const def = AGENT_DEFS.find((d) => d.bin === bin) - if (!def) return command + if (!def) {return command} const flagPattern = new RegExp(`(^|\\s)${escapeRegExp(def.modelFlag)}(?:=|\\s+)\\S+`) if (flagPattern.test(command)) { return command.replace(flagPattern, `$1${def.modelFlag} ${def.judgeModel}`) @@ -48,9 +48,9 @@ export type JudgeOutput = { } function validCandidateId(id: unknown, aCount: number, bCount: number): id is string { - if (typeof id !== 'string') return false + if (typeof id !== 'string') {return false} const match = /^([AB])(\d+)$/.exec(id) - if (!match) return false + if (!match) {return false} const index = Number(match[2]) return match[1] === 'A' ? index < aCount : index < bCount } @@ -66,22 +66,22 @@ export function sanitizeJudgeOutput(raw: unknown, aCount: number, bCount: number // a cycle is dropped, first link wins. const closesCycle = (from: string, to: string): boolean => { for (let current: string | undefined = to; current !== undefined; current = duplicateLinks.get(current)) { - if (current === from) return true + if (current === from) {return true} } return false } for (const item of Array.isArray(r.decisions) ? r.decisions : []) { - if (!item || typeof item !== 'object') continue + if (!item || typeof item !== 'object') {continue} const d = item as Record - if (!validCandidateId(d.id, aCount, bCount) || seen.has(d.id)) continue - if (d.action !== 'keep' && d.action !== 'reject') continue + if (!validCandidateId(d.id, aCount, bCount) || seen.has(d.id)) {continue} + if (d.action !== 'keep' && d.action !== 'reject') {continue} seen.add(d.id) const target = validCandidateId(d.duplicate_of, aCount, bCount) && d.duplicate_of !== d.id ? d.duplicate_of : undefined const duplicateOf = target !== undefined && !closesCycle(d.id, target) ? target : undefined - if (duplicateOf !== undefined) duplicateLinks.set(d.id, duplicateOf) + if (duplicateOf !== undefined) {duplicateLinks.set(d.id, duplicateOf)} const reason = typeof d.reason === 'string' ? d.reason.trim().slice(0, JUDGE_REASON_MAX) || undefined : undefined - const severity = SEVERITIES.includes(d.severity as FindingSeverity) ? (d.severity as FindingSeverity) : undefined + const severity = SEVERITIES.has(d.severity as FindingSeverity) ? (d.severity as FindingSeverity) : undefined decisions.push({ id: d.id, action: d.action, @@ -100,7 +100,7 @@ export function parsePartialJudge( bCount: number, ): { decisions: JudgeDecision[] } | null { const repaired = repairTruncatedJson(text) - if (!repaired) return null + if (!repaired) {return null} let parsed: unknown try { parsed = JSON.parse(repaired) @@ -130,7 +130,7 @@ export function dedupeExactCrossLane(a: SanitizedReview, b: SanitizedReview): Cr const aIndexByKey = new Map() a.findings.forEach((finding, index) => { const key = keyOf(finding) - if (key !== null && !aIndexByKey.has(key)) aIndexByKey.set(key, index) + if (key !== null && !aIndexByKey.has(key)) {aIndexByKey.set(key, index)} }) const aFindings = [...a.findings] @@ -198,7 +198,7 @@ export function assembleDualReview(a: SanitizedReview, b: SanitizedReview, judge let current = id for (;;) { const next = decisionById.get(current)?.duplicate_of - if (!next || !candidateById.has(next) || visited.has(next)) return current + if (!next || !candidateById.has(next) || visited.has(next)) {return current} visited.add(next) current = next } @@ -224,7 +224,7 @@ export function assembleDualReview(a: SanitizedReview, b: SanitizedReview, judge // A rejected root must not speak for members it absorbed: a security // finding, or one the judge explicitly kept, takes over instead of being // silently dropped with the root. - if (rejected) representative = securityMember ?? keptMember ?? representative + if (rejected) {representative = securityMember ?? keptMember ?? representative} const group: Group = { representative, members, @@ -232,13 +232,13 @@ export function assembleDualReview(a: SanitizedReview, b: SanitizedReview, judge survives: !rejected || securityMember !== undefined || keptMember !== undefined, } groups.push(group) - for (const member of members) groupByMemberId.set(member.id, group) + for (const member of members) {groupByMemberId.set(member.id, group)} } const stats: DualStats = { merged: 0, rejected: 0, added_by_b: 0 } for (const group of groups) { - if (!group.survives) stats.rejected += group.members.length - else stats.merged += group.members.length - 1 + if (!group.survives) {stats.rejected += group.members.length} + else {stats.merged += group.members.length - 1} } stats.added_by_b = groups.filter((g) => g.survives && g.representative.side === 'B' && !g.consensus).length @@ -257,15 +257,15 @@ export function assembleDualReview(a: SanitizedReview, b: SanitizedReview, judge const finalIndexByGroup = new Map() for (const candidate of candidates) { const group = groupByMemberId.get(candidate.id) as Group - if (!group.survives || group.representative.id !== candidate.id) continue + if (!group.survives || group.representative.id !== candidate.id) {continue} finalIndexByGroup.set(group, findings.length) findings.push(mergedFinding(group)) } const newIndexByAIndex = new Map() for (const candidate of candidates) { - if (candidate.side !== 'A') continue + if (candidate.side !== 'A') {continue} const finalIndex = finalIndexByGroup.get(groupByMemberId.get(candidate.id) as Group) - if (finalIndex !== undefined) newIndexByAIndex.set(candidate.index, finalIndex) + if (finalIndex !== undefined) {newIndexByAIndex.set(candidate.index, finalIndex)} } let narrative = a.narrative @@ -292,11 +292,11 @@ export function assembleDualReview(a: SanitizedReview, b: SanitizedReview, judge // all still speaks (an approve without findings is legitimate). const laneSurvived = { A: false, B: false } for (const candidate of candidates) { - if ((groupByMemberId.get(candidate.id) as Group).survives) laneSurvived[candidate.side] = true + if ((groupByMemberId.get(candidate.id) as Group).survives) {laneSurvived[candidate.side] = true} } const verdicts: Verdict[] = [] - if (a.findings.length === 0 || laneSurvived.A) verdicts.push(a.verdict) - if (b.findings.length === 0 || laneSurvived.B) verdicts.push(b.verdict) + if (a.findings.length === 0 || laneSurvived.A) {verdicts.push(a.verdict)} + if (b.findings.length === 0 || laneSurvived.B) {verdicts.push(b.verdict)} const verdict = verdicts.length > 0 ? verdicts.reduce(worstVerdict) : 'comment' const files_reviewed = diff --git a/packages/cli/src/export.ts b/packages/cli/src/export.ts index 0f6661f..18c5de4 100644 --- a/packages/cli/src/export.ts +++ b/packages/cli/src/export.ts @@ -7,9 +7,9 @@ import { t } from './i18n.js' import { resolveRecord } from './record.js' function verdictLabel(verdict: string): string { - if (verdict === 'approve') return t('export.verdictApprove') - if (verdict === 'request_changes') return t('export.verdictChanges') - if (verdict === 'comment') return t('export.verdictComment') + if (verdict === 'approve') {return t('export.verdictApprove')} + if (verdict === 'request_changes') {return t('export.verdictChanges')} + if (verdict === 'comment') {return t('export.verdictComment')} return verdict } @@ -22,9 +22,9 @@ function renderFinding(f: Finding, index: number): string { const parts: string[] = [] const badge = [f.severity, f.kind].filter(Boolean).join(' / ') parts.push(`### ${index + 1}. ${findingAnchor(f)} — ${badge}`) - if (f.title) parts.push(`**${f.title}**`) + if (f.title) {parts.push(`**${f.title}**`)} parts.push(f.message) - if (f.suggestion) parts.push('```suggestion\n' + f.suggestion + '\n```') + if (f.suggestion) {parts.push('```suggestion\n' + f.suggestion + '\n```')} return parts.join('\n\n') } @@ -49,13 +49,13 @@ export function renderMarkdown(record: ReviewRecord): string { } if (n) { - if (n.intent) out.push(`**${t('export.intent')}:** ${n.intent} _(${t('export.confidence')}: ${n.confidence})_`) + if (n.intent) {out.push(`**${t('export.intent')}:** ${n.intent} _(${t('export.confidence')}: ${n.confidence})_`)} if (n.prologue) { out.push(`## ${t('export.prologue')}`) const p: string[] = [] - if (n.prologue.why) p.push(`**${t('export.why')}:** ${n.prologue.why}`) - if (n.prologue.what) p.push(`**${t('export.what')}:** ${n.prologue.what}`) - for (const kc of n.prologue.key_changes) p.push(`- **${kc.title}**${kc.detail ? ` — ${kc.detail}` : ''}`) + if (n.prologue.why) {p.push(`**${t('export.why')}:** ${n.prologue.why}`)} + if (n.prologue.what) {p.push(`**${t('export.what')}:** ${n.prologue.what}`)} + for (const kc of n.prologue.key_changes) {p.push(`- **${kc.title}**${kc.detail ? ` — ${kc.detail}` : ''}`)} out.push(p.join('\n\n')) } if (n.review_first.length) { @@ -71,10 +71,10 @@ export function renderMarkdown(record: ReviewRecord): string { n.steps.forEach((ch, i) => { const head = `### ${i + 1}. ${ch.title}${ch.risk ? ` — ${t('export.risk', { risk: t(`risk.${ch.risk}`) })}` : ''}` const body: string[] = [head] - if (ch.rationale) body.push(ch.rationale) - if (ch.take) body.push(`> ${ch.take}`) - if (ch.check) body.push(`- [ ] ${t('export.toVerify')}: ${ch.check}`) - if (ch.files.length) body.push(`${t('export.files')}: ${ch.files.map((f) => `\`${f}\``).join(', ')}`) + if (ch.rationale) {body.push(ch.rationale)} + if (ch.take) {body.push(`> ${ch.take}`)} + if (ch.check) {body.push(`- [ ] ${t('export.toVerify')}: ${ch.check}`)} + if (ch.files.length) {body.push(`${t('export.files')}: ${ch.files.map((f) => `\`${f}\``).join(', ')}`)} if (ch.finding_refs.length) { body.push(`${t('export.findingsRefs')}: ${ch.finding_refs.map((r) => `#${r + 1}`).join(', ')}`) } diff --git a/packages/cli/src/fix.test.ts b/packages/cli/src/fix.test.ts index ced8cd7..6a567bb 100644 --- a/packages/cli/src/fix.test.ts +++ b/packages/cli/src/fix.test.ts @@ -91,7 +91,7 @@ describe('createFixRunner', () => { }) const started = runner.start([0, 1]) expect(started.ok).toBe(true) - while (runner.status().phase === 'running') await new Promise((r) => setTimeout(r, 5)) + while (runner.status().phase === 'running') {await new Promise((r) => setTimeout(r, 5))} expect(runner.status()).toMatchObject({ phase: 'done', summary: 'two files patched', selected: [0, 1] }) expect(seenPrompt).toContain('broken null check') expect(seenCommand).toBe('claude -p --permission-mode acceptEdits') @@ -123,7 +123,7 @@ describe('createFixRunner', () => { expect(runner.start([0]).ok).toBe(true) expect(runner.start([0])).toMatchObject({ ok: false, code: 409 }) release() - while (runner.status().phase === 'running') await new Promise((r) => setTimeout(r, 5)) + while (runner.status().phase === 'running') {await new Promise((r) => setTimeout(r, 5))} expect(runner.start([1]).ok).toBe(true) }) @@ -134,7 +134,7 @@ describe('createFixRunner', () => { }, }) expect(runner.start([0]).ok).toBe(true) - while (runner.status().phase === 'running') await new Promise((r) => setTimeout(r, 5)) + while (runner.status().phase === 'running') {await new Promise((r) => setTimeout(r, 5))} expect(runner.status()).toMatchObject({ phase: 'error', error: 'agent exploded' }) expect(runner.start([0]).ok).toBe(true) }) diff --git a/packages/cli/src/fix.ts b/packages/cli/src/fix.ts index 41c0447..6899c25 100644 --- a/packages/cli/src/fix.ts +++ b/packages/cli/src/fix.ts @@ -15,28 +15,29 @@ const MAX_SUMMARY_CHARS = 4000 */ export function fixCommandFor(command: string): string { if (/^claude(\s|$)/.test(command)) { - if (command.includes('--permission-mode')) return command + if (command.includes('--permission-mode')) {return command} return `${command} --permission-mode acceptEdits` } if (/^codex\s+exec(\s|$)/.test(command)) { - if (command.includes('--sandbox') || command.includes('--full-auto')) return command + if (command.includes('--sandbox') || command.includes('--full-auto')) {return command} return command.replace(/^codex\s+exec/, 'codex exec --sandbox workspace-write') } if (/^gemini(\s|$)/.test(command)) { - if (command.includes('--approval-mode') || command.includes('--yolo')) return command + if (command.includes('--approval-mode') || command.includes('--yolo')) {return command} return `${command} --approval-mode auto_edit` } return command } function isFixable(finding: Finding): boolean { - if (finding.kind === 'praise' || finding.kind === 'why') return false + if (finding.kind === 'praise' || finding.kind === 'why') {return false} return finding.severity !== 'info' } export function buildAgentFixPrompt(record: ReviewRecord, ids: number[]): string { - const findings = ids.map((id) => { - const f = record.review.findings[id]! + const findings = ids.flatMap((id) => { + const f = record.review.findings[id] + if (!f) {return []} return { file: f.file, ...(f.line !== undefined ? { line: f.line } : {}), @@ -102,18 +103,18 @@ export function createFixRunner(opts: { function headMoved(): boolean { const reviewedSha = opts.getRecord()?.meta.head_sha - if (!reviewedSha) return false + if (!reviewedSha) {return false} const head = currentHead() return head !== null && head.trim() !== reviewedSha } function validate(record: ReviewRecord, ids: number[]): string | null { - if (!Array.isArray(ids) || ids.length === 0) return 'no findings selected' + if (!Array.isArray(ids) || ids.length === 0) {return 'no findings selected'} for (const id of ids) { - if (!Number.isInteger(id)) return 'invalid finding id' + if (!Number.isInteger(id)) {return 'invalid finding id'} const finding = record.review.findings[id] - if (!finding) return 'unknown finding id' - if (!isFixable(finding)) return 'finding has nothing to fix' + if (!finding) {return 'unknown finding id'} + if (!isFixable(finding)) {return 'finding has nothing to fix'} } return null } @@ -129,11 +130,11 @@ export function createFixRunner(opts: { head_moved: headMoved(), }), start(ids) { - if (phase === 'running') return { ok: false, code: 409, error: 'a fix is already running' } + if (phase === 'running') {return { ok: false, code: 409, error: 'a fix is already running' }} const record = opts.getRecord() - if (!record) return { ok: false, code: 409, error: 'no review available yet' } + if (!record) {return { ok: false, code: 409, error: 'no review available yet' }} const invalid = validate(record, ids) - if (invalid) return { ok: false, code: 400, error: invalid } + if (invalid) {return { ok: false, code: 400, error: invalid }} phase = 'running' selected = [...ids] diff --git a/packages/cli/src/git.ts b/packages/cli/src/git.ts index 983860b..403d439 100644 --- a/packages/cli/src/git.ts +++ b/packages/cli/src/git.ts @@ -12,7 +12,7 @@ export function git(args: string[], cwd: string): string { }).trimEnd() } catch (err) { if ((err as NodeJS.ErrnoException).code === 'ENOENT') { - throw new Error(t('git.notFound')) + throw new Error(t('git.notFound'), { cause: err }) } throw err } @@ -61,7 +61,7 @@ export function isAncestor(a: string, b: string, cwd: string): boolean { export function revListCount(range: string, cwd: string): number | null { const out = tryGit(['rev-list', '--count', range], cwd) - if (out === null) return null + if (out === null) {return null} const n = Number(out) return Number.isFinite(n) ? n : null } diff --git a/packages/cli/src/i18n.ts b/packages/cli/src/i18n.ts index 1d790ac..1bc8bfc 100644 --- a/packages/cli/src/i18n.ts +++ b/packages/cli/src/i18n.ts @@ -672,7 +672,7 @@ export function t(key: MessageKey, params?: Record, count?: num msg = (n === 1 ? parts[0] : parts[1] ?? parts[0]) ?? msg } if (params) { - for (const [k, v] of Object.entries(params)) msg = msg.replaceAll(`{${k}}`, String(v)) + for (const [k, v] of Object.entries(params)) {msg = msg.replaceAll(`{${k}}`, String(v))} } return msg } diff --git a/packages/cli/src/impact.ts b/packages/cli/src/impact.ts index 56fcf55..93bfe1a 100644 --- a/packages/cli/src/impact.ts +++ b/packages/cli/src/impact.ts @@ -41,10 +41,10 @@ const PYTHON_DECLARATIONS = [/^(?:async\s+)?def\s+([A-Za-z_]\w*)/, /^class\s+([A function declarationPatterns(file: string): RegExp[] { const dot = file.lastIndexOf('.') - if (dot < 0) return [] + if (dot < 0) {return []} const ext = file.slice(dot + 1).toLowerCase() - if (TS_JS_EXTENSIONS.has(ext)) return TS_JS_DECLARATIONS - if (ext === 'py') return PYTHON_DECLARATIONS + if (TS_JS_EXTENSIONS.has(ext)) {return TS_JS_DECLARATIONS} + if (ext === 'py') {return PYTHON_DECLARATIONS} return [] } @@ -54,8 +54,8 @@ function isUsableName(name: string): boolean { function parseDiffPath(raw: string): string | null { const path = raw.trim() - if (path === '/dev/null') return null - if (path.startsWith('a/') || path.startsWith('b/')) return path.slice(2) + if (path === '/dev/null') {return null} + if (path.startsWith('a/') || path.startsWith('b/')) {return path.slice(2)} return path } @@ -67,7 +67,7 @@ export function diffFilePaths(diff: string): string[] { minusPath = parseDiffPath(line.slice(4)) } else if (line.startsWith('+++ ')) { const file = parseDiffPath(line.slice(4)) ?? minusPath - if (file && !files.includes(file)) files.push(file) + if (file && !files.includes(file)) {files.push(file)} } } return files @@ -93,7 +93,7 @@ export function changedSymbolsFromDiff(diff: string): ChangedSymbol[] { const record = (content: string, side: 'minus' | 'plus') => { for (const pattern of patterns) { const name = pattern.exec(content)?.[1] - if (!name || !isUsableName(name)) continue + if (!name || !isUsableName(name)) {continue} const entry = sides.get(name) ?? { minus: false, plus: false } entry[side] = true sides.set(name, entry) @@ -112,8 +112,8 @@ export function changedSymbolsFromDiff(diff: string): ChangedSymbol[] { file = parseDiffPath(line.slice(4)) ?? minusPath patterns = file ? declarationPatterns(file) : [] } else if (file && patterns.length > 0) { - if (line.startsWith('+')) record(line.slice(1), 'plus') - else if (line.startsWith('-')) record(line.slice(1), 'minus') + if (line.startsWith('+')) {record(line.slice(1), 'plus')} + else if (line.startsWith('-')) {record(line.slice(1), 'minus')} } } flush() @@ -122,13 +122,13 @@ export function changedSymbolsFromDiff(diff: string): ChangedSymbol[] { function grepUsages(name: string, excludes: string[], cwd: string): string[] { const out = tryGit(['grep', '-n', '--word-regexp', '--fixed-strings', '-e', name, '--', '.', ...excludes], cwd) - if (!out) return [] + if (!out) {return []} const usages: string[] = [] for (const line of out.split('\n')) { const match = /^(.+?):(\d+):/.exec(line) - if (!match) continue + if (!match) {continue} usages.push(`${match[1]}:${match[2]}`) - if (usages.length >= MAX_USED_AT_PER_SYMBOL) break + if (usages.length >= MAX_USED_AT_PER_SYMBOL) {break} } return usages } @@ -136,16 +136,16 @@ function grepUsages(name: string, excludes: string[], cwd: string): string[] { function grepImporters(file: string, excludes: string[], cwd: string): string[] { const basename = file.split('/').pop() ?? file const stem = basename.replace(/\.[^.]+$/, '') - if (stem.length < MIN_NAME_LENGTH || GENERIC_BASENAMES.has(stem.toLowerCase())) return [] + if (stem.length < MIN_NAME_LENGTH || GENERIC_BASENAMES.has(stem.toLowerCase())) {return []} const out = tryGit(['grep', '-n', '--word-regexp', '--fixed-strings', '-e', stem, '--', '.', ...excludes], cwd) - if (!out) return [] + if (!out) {return []} const importers: string[] = [] for (const line of out.split('\n')) { const match = /^(.+?):\d+:(.*)$/.exec(line) - if (!match || match[1] === undefined || match[2] === undefined) continue - if (!/\b(import|require|from|include|use)\b/.test(match[2])) continue - if (!importers.includes(match[1])) importers.push(match[1]) - if (importers.length >= MAX_IMPORTERS_PER_FILE) break + if (!match || match[1] === undefined || match[2] === undefined) {continue} + if (!/\b(import|require|from|include|use)\b/.test(match[2])) {continue} + if (!importers.includes(match[1])) {importers.push(match[1])} + if (importers.length >= MAX_IMPORTERS_PER_FILE) {break} } return importers } @@ -167,11 +167,11 @@ export function buildImpactCandidates(diff: string, cwd: string): ImpactCandidat const importedBy: Record = {} for (const file of diffFiles) { - if (declarationPatterns(file).length === 0) continue + if (declarationPatterns(file).length === 0) {continue} const importers = grepImporters(file, excludes, cwd) - if (importers.length > 0) importedBy[file] = importers + if (importers.length > 0) {importedBy[file] = importers} } - if (symbols.length === 0 && Object.keys(importedBy).length === 0) return null + if (symbols.length === 0 && Object.keys(importedBy).length === 0) {return null} return { note: IMPACT_NOTE, symbols, imported_by: importedBy } } diff --git a/packages/cli/src/index.ts b/packages/cli/src/index.ts index 2191570..4d29d1c 100644 --- a/packages/cli/src/index.ts +++ b/packages/cli/src/index.ts @@ -14,7 +14,7 @@ import { VERSION } from './version.js' import { configCommand } from './wizard.js' function parseIntFlag(name: string, raw: string | undefined, min: number, max: number): number | undefined { - if (raw === undefined) return undefined + if (raw === undefined) {return undefined} const n = Number(raw) if (!Number.isInteger(n) || n < min || n > max) { throw new Error(t('cli.intFlagError', { name, raw, min, max })) @@ -23,8 +23,8 @@ function parseIntFlag(name: string, raw: string | undefined, min: number, max: n } function parseFailOn(raw: string | undefined): ReviewGate | undefined { - if (raw === undefined) return undefined - if ((REVIEW_GATE_VALUES as readonly string[]).includes(raw)) return raw as ReviewGate + if (raw === undefined) {return undefined} + if ((REVIEW_GATE_VALUES as readonly string[]).includes(raw)) {return raw as ReviewGate} throw new Error(t('cli.failOnError', { raw, values: REVIEW_GATE_VALUES.join(', ') })) } diff --git a/packages/cli/src/menu.ts b/packages/cli/src/menu.ts index dbe1f8e..8da230a 100644 --- a/packages/cli/src/menu.ts +++ b/packages/cli/src/menu.ts @@ -122,7 +122,7 @@ async function runCloudMenu(cwd: string, actions: MenuActions): Promise { })), summary: false, }) - if (picked === null || picked === 'back') return + if (picked === null || picked === 'back') {return} if (picked === 'sync' && !context.inRepo) { printNotInRepo() @@ -157,7 +157,7 @@ export async function runMenu(opts: { cwd: string }): Promise { })), summary: false, }) - if (picked === null || picked === 'quit') return + if (picked === null || picked === 'quit') {return} if ((picked === 'review' || picked === 'dualReview' || picked === 'show') && !context.inRepo) { printNotInRepo() diff --git a/packages/cli/src/notify.ts b/packages/cli/src/notify.ts index 624a391..31959bf 100644 --- a/packages/cli/src/notify.ts +++ b/packages/cli/src/notify.ts @@ -11,7 +11,7 @@ export function notifyDesktop(title: string, body: string): void { : process.platform === 'linux' ? { cmd: 'notify-send', args: ['--app-name=codesema', title, body] } : null - if (!command) return + if (!command) {return} try { spawn(command.cmd, command.args, { stdio: 'ignore', detached: true }).unref() } catch { diff --git a/packages/cli/src/partial.test.ts b/packages/cli/src/partial.test.ts index ae626cd..85d3e18 100644 --- a/packages/cli/src/partial.test.ts +++ b/packages/cli/src/partial.test.ts @@ -75,7 +75,7 @@ describe('parsePartialReview', () => { test('progressive prefix: each slice parses or returns null, without throwing', () => { for (let cut = 1; cut <= FULL.length; cut++) { const partial = parsePartialReview(FULL.slice(0, cut)) - if (cut === FULL.length) expect(partial?.findings).toHaveLength(2) + if (cut === FULL.length) {expect(partial?.findings).toHaveLength(2)} } }) diff --git a/packages/cli/src/partial.ts b/packages/cli/src/partial.ts index 00fdc20..fee84a0 100644 --- a/packages/cli/src/partial.ts +++ b/packages/cli/src/partial.ts @@ -20,18 +20,18 @@ const CLOSED_STRING_TAIL = /^"(?:[^"\\]|\\.)*"$/ function lastNonWhitespace(s: string, before: number): string { for (let i = before - 1; i >= 0; i--) { - const ch = s[i]! - if (ch !== ' ' && ch !== '\n' && ch !== '\r' && ch !== '\t') return ch + const ch = s.charAt(i) + if (ch !== ' ' && ch !== '\n' && ch !== '\r' && ch !== '\t') {return ch} } return '' } function stringStartBackwards(s: string): number { for (let i = s.length - 2; i >= 0; i--) { - if (s[i] !== '"') continue + if (s[i] !== '"') {continue} let backslashes = 0 - for (let j = i - 1; j >= 0 && s[j] === '\\'; j--) backslashes++ - if (backslashes % 2 === 0) return i + for (let j = i - 1; j >= 0 && s[j] === '\\'; j--) {backslashes++} + if (backslashes % 2 === 0) {return i} } return -1 } @@ -43,7 +43,7 @@ function stringStartBackwards(s: string): number { */ export function repairTruncatedJson(raw: string): string | null { const start = raw.indexOf('{') - if (start < 0) return null + if (start < 0) {return null} let s = raw.slice(start) const stack: string[] = [] @@ -53,11 +53,11 @@ export function repairTruncatedJson(raw: string): string | null { let lastStructural = -1 for (let i = 0; i < s.length; i++) { - const ch = s[i]! + const ch = s.charAt(i) if (inString) { - if (escaped) escaped = false - else if (ch === '\\') escaped = true - else if (ch === '"') inString = false + if (escaped) {escaped = false} + else if (ch === '\\') {escaped = true} + else if (ch === '"') {inString = false} continue } if (ch === '"') { @@ -76,24 +76,24 @@ export function repairTruncatedJson(raw: string): string | null { } if (ch === '}' || ch === ']') { stack.pop() - if (stack.length === 0) return s.slice(0, i + 1) + if (stack.length === 0) {return s.slice(0, i + 1)} } } if (inString) { - if (escaped) s = s.slice(0, -1) + if (escaped) {s = s.slice(0, -1)} s = s.replace(/\\u[0-9a-fA-F]{0,3}$/, '') s += '"' const prev = lastNonWhitespace(s, stringStart) const isKey = stack[stack.length - 1] === '{' && (prev === '{' || prev === ',') - if (isKey) s = s.slice(0, stringStart) + if (isKey) {s = s.slice(0, stringStart)} } else { const tail = s.slice(lastStructural + 1).trim() if (tail && !LITERAL_TAIL.test(tail)) { if (CLOSED_STRING_TAIL.test(tail)) { const before = s[lastStructural] ?? '' const isKey = stack[stack.length - 1] === '{' && (before === '{' || before === ',') - if (isKey) s = s.slice(0, lastStructural + 1) + if (isKey) {s = s.slice(0, lastStructural + 1)} } else { s = s.slice(0, lastStructural + 1) } @@ -117,14 +117,14 @@ export function repairTruncatedJson(raw: string): string | null { break } - for (let i = stack.length - 1; i >= 0; i--) s += stack[i] === '{' ? '}' : ']' + for (let i = stack.length - 1; i >= 0; i--) {s += stack[i] === '{' ? '}' : ']'} return s } /** Tolerant extraction of the fields already readable from the review in progress. */ export function parsePartialReview(raw: string): PartialReview | null { const repaired = repairTruncatedJson(raw) - if (!repaired) return null + if (!repaired) {return null} let parsed: unknown try { @@ -132,7 +132,7 @@ export function parsePartialReview(raw: string): PartialReview | null { } catch { return null } - if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) return null + if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {return null} const r = parsed as Record const verdict = @@ -142,9 +142,9 @@ export function parsePartialReview(raw: string): PartialReview | null { const findings: PartialFinding[] = [] if (Array.isArray(r.findings)) { for (const item of r.findings.slice(0, 200)) { - if (!item || typeof item !== 'object') continue + if (!item || typeof item !== 'object') {continue} const f = item as Record - if (typeof f.file !== 'string' || !f.file || typeof f.message !== 'string' || !f.message) continue + if (typeof f.file !== 'string' || !f.file || typeof f.message !== 'string' || !f.message) {continue} findings.push({ file: f.file, message: f.message, @@ -166,6 +166,6 @@ export function parsePartialReview(raw: string): PartialReview | null { : [] const intent = typeof narrative?.intent === 'string' && narrative.intent.trim() ? narrative.intent.trim() : undefined - if (!verdict && !summary && !intent && findings.length === 0 && stepTitles.length === 0) return null + if (!verdict && !summary && !intent && findings.length === 0 && stepTitles.length === 0) {return null} return { verdict, summary, intent, findings, stepTitles } } diff --git a/packages/cli/src/prep.ts b/packages/cli/src/prep.ts index 0a43cd1..d6a927b 100644 --- a/packages/cli/src/prep.ts +++ b/packages/cli/src/prep.ts @@ -44,8 +44,8 @@ export type PrepInput = { } function resolveRef(name: string, cwd: string): string | null { - if (refExists(name, cwd)) return name - if (refExists(`origin/${name}`, cwd)) return `origin/${name}` + if (refExists(name, cwd)) {return name} + if (refExists(`origin/${name}`, cwd)) {return `origin/${name}`} return null } @@ -67,7 +67,7 @@ function targetFromForge(cwd: string): { target: string; source: string } | null const name = (JSON.parse(glabOut) as { target_branch?: string }).target_branch if (name) { const ref = resolveRef(name, cwd) - if (ref) return { target: ref, source: 'gitlab (glab mr view)' } + if (ref) {return { target: ref, source: 'gitlab (glab mr view)' }} } } catch { // unexpected glab output: fall through to the next fallback @@ -76,16 +76,16 @@ function targetFromForge(cwd: string): { target: string; source: string } | null const ghOut = skipGithub ? null : tryExec('gh', ['pr', 'view', '--json', 'baseRefName', '--jq', '.baseRefName'], cwd) if (ghOut) { const ref = resolveRef(ghOut, cwd) - if (ref) return { target: ref, source: 'github (gh pr view)' } + if (ref) {return { target: ref, source: 'github (gh pr view)' }} } return null } function targetFromOriginHead(cwd: string): { target: string; source: string } | null { const sym = tryGit(['symbolic-ref', 'refs/remotes/origin/HEAD'], cwd) - if (!sym) return null + if (!sym) {return null} const ref = sym.replace('refs/remotes/', '') - if (!refExists(ref, cwd)) return null + if (!refExists(ref, cwd)) {return null} return { target: ref, source: 'origin/HEAD' } } @@ -93,12 +93,12 @@ function targetFromHeuristic(current: string, headRef: string, cwd: string): { t let best: { target: string; distance: number } | null = null for (const name of TARGET_CANDIDATES) { const ref = resolveRef(name, cwd) - if (!ref || sameBranch(ref, current)) continue + if (!ref || sameBranch(ref, current)) {continue} const mb = mergeBase(ref, headRef, cwd) - if (!mb) continue + if (!mb) {continue} const distance = revListCount(`${mb}..${headRef}`, cwd) - if (distance === null) continue - if (!best || distance < best.distance) best = { target: ref, distance } + if (distance === null) {continue} + if (!best || distance < best.distance) {best = { target: ref, distance }} } return best ? { target: best.target, source: 'heuristic (nearest merge-base)' } : null } @@ -111,7 +111,7 @@ export function detectTarget( ): { target: string; source: string } { if (flag) { const ref = resolveRef(flag, cwd) - if (!ref) throw new Error(t('prep.targetFlagNotFound', { flag })) + if (!ref) {throw new Error(t('prep.targetFlagNotFound', { flag }))} return { target: ref, source: '--target flag' } } const forge = headRef === 'HEAD' ? targetFromForge(cwd) : null @@ -128,7 +128,7 @@ function excludePathspecs(cwd: string): string[] { if (existsSync(ignoreFile)) { for (const raw of readFileSync(ignoreFile, 'utf8').split('\n')) { const line = raw.trim() - if (!line || line.startsWith('#')) continue + if (!line || line.startsWith('#')) {continue} patterns.push(line) } } @@ -236,7 +236,7 @@ export function prep(opts: { branch?: string; target?: string; cwd: string; quie ...(custom ? [{ label: t('prep.label.custom'), value: t('prep.customNote') }] : []), { label: t('prep.label.input'), value: inputPath }, ] - for (const line of renderFieldRows(rows)) console.log(line) + for (const line of renderFieldRows(rows)) {console.log(line)} console.log('') console.log(t('prep.next')) } diff --git a/packages/cli/src/record.test.ts b/packages/cli/src/record.test.ts index 35bcd7b..7e8b6b3 100644 --- a/packages/cli/src/record.test.ts +++ b/packages/cli/src/record.test.ts @@ -35,7 +35,7 @@ describe('archiveRecord', () => { archiveRecord(record({ branch: 'feat/x', target: 'develop' }), dir) const names = readdirSync(reviewsDir) - const kept = names.filter((n) => /^feat-x-\d{8}-\d{6}\.json$/.test(n)).sort() + const kept = names.filter((n) => /^feat-x-\d{8}-\d{6}\.json$/.test(n)).toSorted() expect(kept.length).toBe(5) expect(kept).not.toContain('feat-x-20260101-000000.json') expect(kept).not.toContain('feat-x-20260102-000000.json') diff --git a/packages/cli/src/record.ts b/packages/cli/src/record.ts index 907e89f..98334eb 100644 --- a/packages/cli/src/record.ts +++ b/packages/cli/src/record.ts @@ -23,7 +23,7 @@ function archiveNames(reviewsDir: string, slugged: string): string[] { const stampTail = /^\d{8}-\d{6}\.json$/ return readdirSync(reviewsDir) .filter((n) => n.startsWith(`${slugged}-`) && stampTail.test(n.slice(slugged.length + 1))) - .sort() + .toSorted() } export function archiveRecord(record: ReviewRecord, cwd: string): string { @@ -55,7 +55,7 @@ function buildRecord(agentOutputPath: string, dir: string): ReviewRecord { } const raw = readJson(inputPath) const input = (raw && typeof raw === 'object' ? raw : {}) as Record - return sanitizeRecord({ + const record = sanitizeRecord({ meta: { title: input.title, branch: input.branch, @@ -67,17 +67,23 @@ function buildRecord(agentOutputPath: string, dir: string): ReviewRecord { commits: input.commits, diff: input.diff, review: readJson(agentOutputPath), - })! + }) + if (!record) { + throw new Error(t('record.invalidJson', { path: agentOutputPath })) + } + return record } function latestSavedRecord(reviewsDir: string): { record: ReviewRecord; path: string } | null { - if (!existsSync(reviewsDir)) return null - const names = readdirSync(reviewsDir).filter((n) => n.endsWith('.json')).sort() + if (!existsSync(reviewsDir)) {return null} + const names = readdirSync(reviewsDir).filter((n) => n.endsWith('.json')).toSorted() for (let i = names.length - 1; i >= 0; i--) { - const path = join(reviewsDir, names[i]!) + const name = names[i] + if (!name) {continue} + const path = join(reviewsDir, name) try { const record = sanitizeRecord(readJson(path)) - if (record) return { record, path } + if (record) {return { record, path }} } catch { // unreadable archive: fall back to the previous one } @@ -88,8 +94,8 @@ function latestSavedRecord(reviewsDir: string): { record: ReviewRecord; path: st /** Last archived review of this branch to this target, with a known head_sha. */ export function findPreviousReview(cwd: string, branch: string, target: string): ReviewRecord | null { const reviewsDir = join(cwd, '.codesema', 'reviews') - if (!existsSync(reviewsDir)) return null - const names = archiveNames(reviewsDir, slug(branch)).reverse() + if (!existsSync(reviewsDir)) {return null} + const names = archiveNames(reviewsDir, slug(branch)).toReversed() for (const name of names) { try { const record = sanitizeRecord(readJson(join(reviewsDir, name))) diff --git a/packages/cli/src/review.test.ts b/packages/cli/src/review.test.ts index b87647f..43fb651 100644 --- a/packages/cli/src/review.test.ts +++ b/packages/cli/src/review.test.ts @@ -212,7 +212,7 @@ describe('runDualFlow', () => { const tempDirs: string[] = [] afterAll(() => { - for (const dir of tempDirs) rmSync(dir, { recursive: true, force: true }) + for (const dir of tempDirs) {rmSync(dir, { recursive: true, force: true })} }) function setupDualRepo(agentPayload: string) { @@ -271,7 +271,7 @@ describe('runDualFlow', () => { const outcome = await runDualFlow(flowOpts(fixture)) expect(outcome.ok).toBe(true) - if (!outcome.ok) return + if (!outcome.ok) {return} expect(outcome.reportLines.filter((line) => line.includes('did not examine'))).toHaveLength(2) }, 20000) @@ -283,7 +283,7 @@ describe('runDualFlow', () => { const outcome = await runDualFlow(flowOpts(fixture)) expect(outcome.ok).toBe(true) - if (!outcome.ok) return + if (!outcome.ok) {return} expect(outcome.record.review.findings).toHaveLength(1) expect(outcome.record.review.findings[0]?.consensus).toBe(true) expect(outcome.record.meta.dual).toEqual({ merged: 1, rejected: 0, added_by_b: 0 }) diff --git a/packages/cli/src/review.ts b/packages/cli/src/review.ts index 6c46f06..cc292a2 100644 --- a/packages/cli/src/review.ts +++ b/packages/cli/src/review.ts @@ -41,12 +41,12 @@ export const REVIEW_GATE_VALUES: readonly ReviewGate[] = ['critical', 'major', ' const SEVERITY_RANK: Record = { info: 0, minor: 1, major: 2, critical: 3 } /** Returns a human reason when the review trips the gate, or null when it passes. */ -export function reviewGateReason(review: SanitizedReview, gate: ReviewGate): string | null { +export function reviewGateReason(sanitized: SanitizedReview, gate: ReviewGate): string | null { if (gate === 'request_changes') { - return review.verdict === 'request_changes' ? t('review.gateReasonVerdict') : null + return sanitized.verdict === 'request_changes' ? t('review.gateReasonVerdict') : null } const threshold = SEVERITY_RANK[gate] - const count = review.findings.filter((f) => SEVERITY_RANK[f.severity] >= threshold).length + const count = sanitized.findings.filter((f) => SEVERITY_RANK[f.severity] >= threshold).length return count > 0 ? t('review.gateReasonSeverity', { n: count, level: gate }) : null } @@ -74,10 +74,10 @@ export function agentVisibleInput(input: PrepInput): { export function groundingReportLines(report: GroundingReport): string[] { const lines: string[] = [] - if (report.dropped.length > 0) lines.push(t('review.groundedDropped', { n: report.dropped.length })) - if (report.deanchored.length > 0) lines.push(t('review.groundedDeanchored', { n: report.deanchored.length })) - if (report.merged > 0) lines.push(t('review.groundedMerged', { n: report.merged })) - if (report.verdict_escalated) lines.push(t('review.groundedVerdict')) + if (report.dropped.length > 0) {lines.push(t('review.groundedDropped', { n: report.dropped.length }))} + if (report.deanchored.length > 0) {lines.push(t('review.groundedDeanchored', { n: report.deanchored.length }))} + if (report.merged > 0) {lines.push(t('review.groundedMerged', { n: report.merged }))} + if (report.verdict_escalated) {lines.push(t('review.groundedVerdict'))} return lines } @@ -166,11 +166,11 @@ Output the FULL updated review JSON (exact same schema), and NOTHING else.` function buildIncrementalPrompt(input: PrepInput, cwd: string): { prompt: string; sinceSha: string } | null { const previous = findPreviousReview(cwd, input.branch, input.target) const since = previous?.meta.head_sha - if (!previous || !since) return null - if (since === input.head_sha) return null - if (!isAncestor(since, input.head_sha, cwd)) return null + if (!previous || !since) {return null} + if (since === input.head_sha) {return null} + if (!isAncestor(since, input.head_sha, cwd)) {return null} const incrementalDiff = mrDiff(`${since}..${input.head_sha}`, cwd) - if (!incrementalDiff.trim()) return null + if (!incrementalDiff.trim()) {return null} const prompt = [ reviewInstructions(), @@ -186,7 +186,7 @@ function buildIncrementalPrompt(input: PrepInput, cwd: string): { prompt: string function detectAgentCommand(cwd: string): string { const [first] = detectAgents(cwd) - if (first) return defaultCommand(first) + if (first) {return defaultCommand(first)} throw new Error(t('agent.noneFound', { bins: AGENT_DEFS.map((d) => d.bin).join(', ') })) } @@ -200,11 +200,11 @@ export function extractReviewJson(raw: string): string { } catch { continue } - if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) continue - if ('verdict' in (parsed as Record)) return candidate + if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {continue} + if ('verdict' in (parsed as Record)) {return candidate} fallback ??= candidate } - if (fallback) return fallback + if (fallback) {return fallback} throw new Error(t('agent.noJsonReview')) } @@ -212,11 +212,11 @@ export function extractReviewJson(raw: string): string { function* jsonCandidates(s: string): Generator { yield s for (const m of s.matchAll(/```(?:json)?\s*([\s\S]*?)```/g)) { - if (m[1]) yield m[1].trim() + if (m[1]) {yield m[1].trim()} } for (let i = s.indexOf('{'); i >= 0; i = s.indexOf('{', i + 1)) { const end = balancedEnd(s, i) - if (end > i) yield s.slice(i, end + 1) + if (end > i) {yield s.slice(i, end + 1)} } } @@ -227,13 +227,13 @@ function balancedEnd(s: string, start: number): number { for (let i = start; i < s.length; i++) { const ch = s[i] if (inString) { - if (ch === '\\') i++ - else if (ch === '"') inString = false - } else if (ch === '"') inString = true - else if (ch === '{') depth++ + if (ch === '\\') {i++} + else if (ch === '"') {inString = false} + } else if (ch === '"') {inString = true} + else if (ch === '{') {depth++} else if (ch === '}') { depth-- - if (depth === 0) return i + if (depth === 0) {return i} } } return -1 @@ -245,12 +245,12 @@ function createPartialForwarder(session: LiveSession, lane: 'a' | 'b' = 'a'): (t let lastParse = 0 return (text: string) => { const now = Date.now() - if (now - lastParse < PARTIAL_PARSE_INTERVAL_MS) return null + if (now - lastParse < PARTIAL_PARSE_INTERVAL_MS) {return null} lastParse = now const partial = parsePartialReview(text) if (partial) { - if (lane === 'a') session.setPartial(partial) - else session.setPartialB(partial) + if (lane === 'a') {session.setPartial(partial)} + else {session.setPartialB(partial)} } return partial } @@ -261,14 +261,14 @@ export function missingReviewedFiles( files: { path: string }[], reviewed: string[] | undefined, ): string[] | null { - if (reviewed === undefined) return null + if (reviewed === undefined) {return null} const seen = new Set(reviewed) return files.map((f) => f.path).filter((path) => !seen.has(path)) } -function coverageGapLine(input: PrepInput, lane: string, review: SanitizedReview): string | null { - const missing = missingReviewedFiles(input.files, review.files_reviewed) - if (!missing || missing.length === 0) return null +function coverageGapLine(input: PrepInput, lane: string, sanitized: SanitizedReview): string | null { + const missing = missingReviewedFiles(input.files, sanitized.files_reviewed) + if (!missing || missing.length === 0) {return null} const shown = missing.slice(0, 3).join(', ') return t('review.coverageGap', { lane, n: missing.length, files: missing.length > 3 ? `${shown}, …` : shown }) } @@ -341,7 +341,7 @@ export async function runDualFlow(opts: { timeoutMs, onText: (text) => { const partial = forward(text) - if (!partial) return + if (!partial) {return} lanes[lane] = progressLabel(partial) updateLanes() }, @@ -358,7 +358,7 @@ export async function runDualFlow(opts: { const settle = ( res: PromiseSettledResult, ): { review: SanitizedReview | null; error: string | null; raw: string | null } => { - if (res.status === 'fulfilled') return { review: res.value, error: null, raw: null } + if (res.status === 'fulfilled') {return { review: res.value, error: null, raw: null }} const message = res.reason instanceof Error ? res.reason.message : String(res.reason) return { review: null, error: message, raw: res.reason instanceof AgentOutputError ? res.reason.raw : null } } @@ -375,8 +375,8 @@ export async function runDualFlow(opts: { } } - const buildRecord = (review: SanitizedReview): ReviewRecord => { - writeFileSync(join(dir, 'review.json'), JSON.stringify(review, null, 2)) + const buildRecord = (sanitized: SanitizedReview): ReviewRecord => { + writeFileSync(join(dir, 'review.json'), JSON.stringify(sanitized, null, 2)) return resolveRecord({ cwd: input.repo_root }).record } @@ -423,10 +423,10 @@ export async function runDualFlow(opts: { timeoutMs, onText: (text) => { const now = Date.now() - if (now - lastJudgeParse < PARTIAL_PARSE_INTERVAL_MS) return + if (now - lastJudgeParse < PARTIAL_PARSE_INTERVAL_MS) {return} lastJudgeParse = now const partial = parsePartialJudge(text, aCount, bCount) - if (!partial) return + if (!partial) {return} session.setJudge({ total, decisions: partial.decisions }) spinner.update(t('review.dualJudgeProgress', { done: partial.decisions.length, total })) }, @@ -479,7 +479,7 @@ export async function runDualFlow(opts: { * true if execution may proceed, false if the user cancels. */ async function ensureRepoAgentTrusted(cwd: string, command: string): Promise { - if (isRepoAgentTrusted(cwd, command)) return true + if (isRepoAgentTrusted(cwd, command)) {return true} if (!isInteractive()) { throw new Error(t('review.repoAgentUnattended', { command })) } @@ -495,7 +495,7 @@ async function ensureRepoAgentTrusted(cwd: string, command: string): Promise console.log(line)) console.log('') - if (opts.open && !opts.failOn) openBrowser(url) + if (opts.open && !opts.failOn) {openBrowser(url)} const shortCmd = agentCommand.length > 40 ? `${agentCommand.slice(0, 37)}…` : agentCommand const spinner = startSpinner(t('review.spinner', { cmd: shortCmd })) @@ -617,11 +617,11 @@ export async function review(opts: { const heading = kind === 'run' ? t('review.runFailed') : t('review.unusableOutput') spinner.stop(` ${paint('✘', RED)} ${heading}`) session.setError(message) - if (isInteractive()) notifyDesktop('codesema', t(kind === 'run' ? 'notify.failedRun' : 'notify.failedOutput')) + if (isInteractive()) {notifyDesktop('codesema', t(kind === 'run' ? 'notify.failedRun' : 'notify.failedOutput'))} console.error(`codesema: ${kind === 'run' ? t('review.runFailedDetail', { message }) : message}`) console.log(` ${t('review.stillUp', { url })}`) process.exitCode = 1 - if (opts.failOn) await stop() + if (opts.failOn) {await stop()} } let record: ReviewRecord @@ -629,7 +629,7 @@ export async function review(opts: { if (dual) { const outcome = await runDualFlow({ agentCommand, input, dir, timeoutMs, session, spinner }) if (!outcome.ok) { - if (outcome.rawOutput !== undefined) writeFileSync(join(dir, 'agent-output.txt'), outcome.rawOutput) + if (outcome.rawOutput !== undefined) {writeFileSync(join(dir, 'agent-output.txt'), outcome.rawOutput)} await failRun(outcome.failure, outcome.message) return } @@ -648,9 +648,9 @@ export async function review(opts: { timeoutMs, onText: (text) => { const partial = forwardPartial(text) - if (!partial) return + if (!partial) {return} const status = progressLabel(partial) - if (status) spinner.update(status) + if (status) {spinner.update(status)} }, }, (raw) => { @@ -680,7 +680,7 @@ export async function review(opts: { // against the full file list would cry wolf. if (!incremental) { const coverage = coverageGapLine(input, t('review.dualLaneA'), grounded.review) - if (coverage) reportLines.push(coverage) + if (coverage) {reportLines.push(coverage)} } } catch (err) { writeFileSync(join(dir, 'agent-output.txt'), out) diff --git a/packages/cli/src/serve.test.ts b/packages/cli/src/serve.test.ts index 96cd8fb..6426377 100644 --- a/packages/cli/src/serve.test.ts +++ b/packages/cli/src/serve.test.ts @@ -136,7 +136,7 @@ function rawRequest( }, ) req.on('error', reject) - if (opts.body !== undefined) req.write(opts.body) + if (opts.body !== undefined) {req.write(opts.body)} req.end() }) } @@ -144,7 +144,7 @@ function rawRequest( async function waitFor(predicate: () => boolean, timeoutMs = 2000): Promise { const startedAt = Date.now() while (!predicate()) { - if (Date.now() - startedAt > timeoutMs) throw new Error('timed out waiting for condition') + if (Date.now() - startedAt > timeoutMs) {throw new Error('timed out waiting for condition')} await new Promise((r) => setTimeout(r, 20)) } } diff --git a/packages/cli/src/serve.ts b/packages/cli/src/serve.ts index c052ed8..4e0dcd5 100644 --- a/packages/cli/src/serve.ts +++ b/packages/cli/src/serve.ts @@ -76,7 +76,7 @@ export function createSession(initial?: { record?: ReviewRecord }): LiveSession } const emit = (event: SessionEvent) => { - for (const listener of listeners) listener(event) + for (const listener of listeners) {listener(event)} } const emitStatus = () => emit({ name: 'status', data: status }) @@ -137,10 +137,10 @@ const LOOPBACK_HOSTNAMES = new Set(['localhost', '127.0.0.1', '[::1]', '::1']) /** Whether the Host header points to loopback (hostname before the port, IPv6 in brackets). */ export function isLoopbackHost(host: string | undefined): boolean { - if (!host) return false + if (!host) {return false} const match = /^(\[[^\]]+\]|[^:]+)(?::\d+)?$/.exec(host.trim()) - if (!match) return false - return LOOPBACK_HOSTNAMES.has(match[1]!.toLowerCase()) + if (!match) {return false} + return LOOPBACK_HOSTNAMES.has((match[1] ?? '').toLowerCase()) } const MIME_BY_EXTENSION: Record = { @@ -165,9 +165,9 @@ export function resolveStaticPath(root: string, pathname: string): string | null } catch { return null } - if (decoded.includes('\0')) return null + if (decoded.includes('\0')) {return null} const resolved = resolve(root, '.' + decoded) - if (resolved !== root && !resolved.startsWith(root + sep)) return null + if (resolved !== root && !resolved.startsWith(root + sep)) {return null} return resolved } @@ -208,12 +208,12 @@ function serveEvents(session: LiveSession, req: IncomingMessage, res: ServerResp send({ name: 'status', data: session.status() }) const partial = session.partial() - if (partial) send({ name: 'partial', data: partial }) + if (partial) {send({ name: 'partial', data: partial })} const partialB = session.partialB() - if (partialB) send({ name: 'partial_b', data: partialB }) + if (partialB) {send({ name: 'partial_b', data: partialB })} const judge = session.judge() - if (judge) send({ name: 'judge', data: judge }) - if (session.status().phase === 'done') send({ name: 'done', data: {} }) + if (judge) {send({ name: 'judge', data: judge })} + if (session.status().phase === 'done') {send({ name: 'done', data: {} })} req.on('close', () => { clearInterval(heartbeat) @@ -256,8 +256,8 @@ type FixEndpoint = { runner: FixRunner; token: string } * served page, unreadable cross-origin) blocks blind CSRF posts to 127.0.0.1. */ async function handleFixStart(req: IncomingMessage, res: ServerResponse, fix: FixEndpoint | undefined): Promise { - if (!fix) return sendJson(res, 501, { error: 'fix runner unavailable' }) - if (req.headers['x-codesema-fix-token'] !== fix.token) return sendText(res, 403, 'forbidden') + if (!fix) {return sendJson(res, 501, { error: 'fix runner unavailable' })} + if (req.headers['x-codesema-fix-token'] !== fix.token) {return sendText(res, 403, 'forbidden')} let body: unknown try { body = await readJsonBody(req, MAX_FIX_BODY_BYTES) @@ -269,13 +269,13 @@ async function handleFixStart(req: IncomingMessage, res: ServerResponse, fix: Fi return sendText(res, 400, 'bad request') } const started = fix.runner.start(findings) - if (!started.ok) return sendJson(res, started.code, { error: started.error }) + if (!started.ok) {return sendJson(res, started.code, { error: started.error })} return sendJson(res, 202, { ok: true }) } async function serveStaticFile(res: ServerResponse, pathname: string): Promise { const filePath = resolveStaticPath(WEB_DIST, pathname) - if (!filePath) return sendText(res, 404, 'not found') + if (!filePath) {return sendText(res, 404, 'not found')} let content: Buffer try { content = await readFile(filePath) @@ -295,7 +295,7 @@ function createRequestHandler(session: LiveSession, indexHtml: string, fix?: Fix // 127.0.0.1 via DNS rebinding (a domain that later resolves to loopback) and // read the diff/review. Accept only requests whose Host header is loopback, so // a rebound domain is rejected. - if (!isLoopbackHost(req.headers.host)) return sendText(res, 403, 'forbidden') + if (!isLoopbackHost(req.headers.host)) {return sendText(res, 403, 'forbidden')} let pathname: string try { @@ -305,10 +305,10 @@ function createRequestHandler(session: LiveSession, indexHtml: string, fix?: Fix } if (req.method === 'POST') { - if (pathname === '/api/fix') return void handleFixStart(req, res, fix) + if (pathname === '/api/fix') {return void handleFixStart(req, res, fix)} return sendText(res, 405, 'method not allowed') } - if (req.method !== 'GET') return sendText(res, 405, 'method not allowed') + if (req.method !== 'GET') {return sendText(res, 405, 'method not allowed')} if (pathname.startsWith('/api/')) { if (pathname === '/api/status') { @@ -316,15 +316,15 @@ function createRequestHandler(session: LiveSession, indexHtml: string, fix?: Fix } if (pathname === '/api/review') { const record = session.record() - if (!record) return sendJson(res, 202, session.status()) + if (!record) {return sendJson(res, 202, session.status())} return sendJson(res, 200, record) } if (pathname === '/api/fix/status') { - if (!fix) return sendJson(res, 200, { available: false }) + if (!fix) {return sendJson(res, 200, { available: false })} return sendJson(res, 200, fix.runner.status()) } if (pathname === '/api/events') { - if (sseClients >= MAX_SSE_CLIENTS) return sendText(res, 503, 'too many event streams') + if (sseClients >= MAX_SSE_CLIENTS) {return sendText(res, 503, 'too many event streams')} sseClients++ return serveEvents(session, req, res, () => { sseClients-- @@ -333,7 +333,7 @@ function createRequestHandler(session: LiveSession, indexHtml: string, fix?: Fix return sendText(res, 404, 'not found') } - if (pathname === '/') return sendHtml(res, indexHtml) + if (pathname === '/') {return sendHtml(res, indexHtml)} void serveStaticFile(res, pathname) } } @@ -347,12 +347,12 @@ async function listen( const ok = await new Promise((resolveListen) => { server.once('error', (err: NodeJS.ErrnoException) => { server.close() - if (err.code !== 'EADDRINUSE') console.error(err.message) + if (err.code !== 'EADDRINUSE') {console.error(err.message)} resolveListen(false) }) server.listen(port, '127.0.0.1', () => resolveListen(true)) }) - if (ok) return { server, port } + if (ok) {return { server, port }} } throw new Error(t('serve.noFreePort', { start: startPort, end: startPort + 19 })) } diff --git a/packages/cli/src/show.ts b/packages/cli/src/show.ts index 5a88dce..1017eff 100644 --- a/packages/cli/src/show.ts +++ b/packages/cli/src/show.ts @@ -44,6 +44,6 @@ export async function show(opts: { review?: string; port?: number; open: boolean console.log(`codesema — ${record.meta.branch} → ${record.meta.target}`) console.log(` ${url}`) console.log(` ${t('review.ctrlc')}`) - if (opts.open) openBrowser(url) + if (opts.open) {openBrowser(url)} printUpdateNotice(await latestVersion) } diff --git a/packages/cli/src/summary.ts b/packages/cli/src/summary.ts index edc78d3..3f8cf98 100644 --- a/packages/cli/src/summary.ts +++ b/packages/cli/src/summary.ts @@ -40,12 +40,12 @@ export function formatFindingCounts(findings: Finding[]): string { const parts: string[] = [] for (const severity of ['critical', 'major', 'minor', 'info'] as const) { const count = countOf(severity) - if (count === 0) continue + if (count === 0) {continue} const text = t(severityKeys[severity], { n: count }) const color = SEVERITY_COLORS[severity] parts.push(color ? paint(text, color) : text) } - if (praise > 0) parts.push(paint(t('summary.praiseCount', { n: praise }), GREEN)) + if (praise > 0) {parts.push(paint(t('summary.praiseCount', { n: praise }), GREEN))} return parts.length > 0 ? parts.join(' · ') : t('summary.none') } @@ -57,7 +57,7 @@ export function printReviewSummary(record: ReviewRecord): void { ]).forEach((line) => console.log(line)) const hotspots = review.narrative?.review_first ?? [] - if (hotspots.length === 0) return + if (hotspots.length === 0) {return} console.log('') console.log(` ${paint(t('summary.checkFirst'), ACCENT)}`) @@ -67,6 +67,6 @@ export function printReviewSummary(record: ReviewRecord): void { const number = dim(`${index + 1}.`.padEnd(3)) const risk = paint(t(`risk.${item.risk}`).padEnd(riskColumnWidth), RISK_COLORS[item.risk]) console.log(` ${number}${risk}${truncate(item.point, POINT_MAX)}`) - if (item.file) console.log(` ${fileIndent}${dim(item.file)}`) + if (item.file) {console.log(` ${fileIndent}${dim(item.file)}`)} }) } diff --git a/packages/cli/src/sync-commands.test.ts b/packages/cli/src/sync-commands.test.ts index e64e226..ef69b2d 100644 --- a/packages/cli/src/sync-commands.test.ts +++ b/packages/cli/src/sync-commands.test.ts @@ -255,10 +255,10 @@ describe('sync and link commands', () => { afterEach(async () => { await stub.close() - if (previousConfigDir === undefined) delete process.env.CODESEMA_CONFIG_DIR - else process.env.CODESEMA_CONFIG_DIR = previousConfigDir - if (previousSyncUrl === undefined) delete process.env.CODESEMA_SYNC_URL - else process.env.CODESEMA_SYNC_URL = previousSyncUrl + if (previousConfigDir === undefined) {delete process.env.CODESEMA_CONFIG_DIR} + else {process.env.CODESEMA_CONFIG_DIR = previousConfigDir} + if (previousSyncUrl === undefined) {delete process.env.CODESEMA_SYNC_URL} + else {process.env.CODESEMA_SYNC_URL = previousSyncUrl} rmSync(configDir, { recursive: true, force: true }) rmSync(repoDir, { recursive: true, force: true }) }) diff --git a/packages/cli/src/sync.test.ts b/packages/cli/src/sync.test.ts index 1afce6d..b13a071 100644 --- a/packages/cli/src/sync.test.ts +++ b/packages/cli/src/sync.test.ts @@ -53,10 +53,10 @@ describe('sync http client', () => { }) afterEach(() => { - if (previousConfigDir === undefined) delete process.env.CODESEMA_CONFIG_DIR - else process.env.CODESEMA_CONFIG_DIR = previousConfigDir - if (previousSyncUrl === undefined) delete process.env.CODESEMA_SYNC_URL - else process.env.CODESEMA_SYNC_URL = previousSyncUrl + if (previousConfigDir === undefined) {delete process.env.CODESEMA_CONFIG_DIR} + else {process.env.CODESEMA_CONFIG_DIR = previousConfigDir} + if (previousSyncUrl === undefined) {delete process.env.CODESEMA_SYNC_URL} + else {process.env.CODESEMA_SYNC_URL = previousSyncUrl} rmSync(configDir, { recursive: true, force: true }) }) diff --git a/packages/cli/src/sync.ts b/packages/cli/src/sync.ts index 7f3934e..57366a4 100644 --- a/packages/cli/src/sync.ts +++ b/packages/cli/src/sync.ts @@ -16,7 +16,7 @@ import { ACCENT, GREEN, bold, dim, paint, renderFieldRows, startSpinner, underli function printOperationResult(statusMessage: string, rows: FieldRow[]): void { console.log('') console.log(` ${paint('✔', GREEN)} ${statusMessage}`) - for (const line of renderFieldRows(rows)) console.log(` ${line}`) + for (const line of renderFieldRows(rows)) {console.log(` ${line}`)} } // The diff carried by a review record is uploaded verbatim on sync. A committed @@ -39,7 +39,7 @@ export function syncBaseUrl(): string { export function loadSyncCredentials(): SyncCredentials | null { const config = loadGlobalConfig() - if (!config.syncWorkspaceId || !config.syncSecret) return null + if (!config.syncWorkspaceId || !config.syncSecret) {return null} // The secret is a bearer token: it is only ever sent to the host it was // created against (stored syncUrl), so a later CODESEMA_SYNC_URL change // cannot redirect it to another server. Credentials saved before the URL @@ -61,7 +61,7 @@ async function api( try { res = await fetchImpl(url, { ...init, - headers: { 'content-type': 'application/json', ...(init.headers ?? {}) }, + headers: { 'content-type': 'application/json', ...init.headers }, signal: AbortSignal.timeout(30_000), }) } catch { @@ -73,7 +73,7 @@ async function api( throw new Error(message) } const parsed = parse(body) - if (parsed === null) throw new Error(t('sync.badResponse', { url })) + if (parsed === null) {throw new Error(t('sync.badResponse', { url }))} return parsed } @@ -141,9 +141,9 @@ export async function autoPushReview( fetchImpl: typeof fetch = fetch, ): Promise { const creds = loadSyncCredentials() - if (!creds || loadGlobalConfig().syncAutoPush !== true) return { status: 'disabled' } + if (!creds || loadGlobalConfig().syncAutoPush !== true) {return { status: 'disabled' }} const secrets = detectDiffSecrets(record.diff) - if (secrets.length > 0) return { status: 'blocked_secrets', count: secrets.length } + if (secrets.length > 0) {return { status: 'blocked_secrets', count: secrets.length }} try { const remoteUrl = tryGit(['remote', 'get-url', 'origin'], cwd) const result = await pushReview({ record, remoteUrl, repoName: basename(cwd) }, creds, fetchImpl) @@ -210,8 +210,8 @@ async function waitForLinkConfirmation( const deadline = Date.now() + LINK_POLL_BACKSTOP_MS for (;;) { const status = await getLinkRequestStatus(code, creds, fetchImpl) - if (status !== 'pending') return status - if (Date.now() > deadline) return 'expired' + if (status !== 'pending') {return status} + if (Date.now() > deadline) {return 'expired'} await new Promise((resolve) => setTimeout(resolve, pollIntervalMs)) } } @@ -234,7 +234,7 @@ export async function deleteWorkspaceData( // (the review record INCLUDING the diff) and asks for confirmation. async function ensureCredentials(): Promise { const existing = loadSyncCredentials() - if (existing) return existing + if (existing) {return existing} if (!isInteractive()) { throw new Error(t('sync.nonInteractiveSetup')) } @@ -249,14 +249,14 @@ async function ensureCredentials(): Promise { ], initialIndex: 0, }) - if (choice !== 'yes') return null + if (choice !== 'yes') {return null} return createWorkspace() } // Asked once, after the first successful manual push: a dismissed prompt // (null) leaves the choice open for the next sync instead of recording a "no". async function offerAutoPush(): Promise { - if (!isInteractive() || loadGlobalConfig().syncAutoPush !== undefined) return + if (!isInteractive() || loadGlobalConfig().syncAutoPush !== undefined) {return} const choice = await select<'yes' | 'no'>({ title: t('sync.autoPushQuestion'), options: [ @@ -265,14 +265,14 @@ async function offerAutoPush(): Promise { ], initialIndex: 0, }) - if (choice === null) return + if (choice === null) {return} saveGlobalConfig({ ...loadGlobalConfig(), syncAutoPush: choice === 'yes' }) } // Deleting remote data is irreversible: every interactive path (menu or direct // `codesema sync delete`) confirms first; non-interactive runs stay scriptable. async function confirmSyncDelete(): Promise { - if (!isInteractive()) return true + if (!isInteractive()) {return true} const choice = await select<'cancel' | 'delete'>({ title: t('menu.syncDeleteConfirm'), options: [ @@ -288,8 +288,8 @@ async function confirmSyncDelete(): Promise { export async function syncCommand(opts: { action?: string; cwd: string; force?: boolean }): Promise { if (opts.action === 'delete') { const creds = loadSyncCredentials() - if (!creds) throw new Error(t('sync.noCredentials')) - if (!(await confirmSyncDelete())) return + if (!creds) {throw new Error(t('sync.noCredentials'))} + if (!(await confirmSyncDelete())) {return} await deleteWorkspaceData(creds) printOperationResult(t('sync.deleted'), []) return @@ -308,7 +308,7 @@ export async function syncCommand(opts: { action?: string; cwd: string; force?: console.log(` ${t('sync.aborted')}`) return } - if (secrets.length > 0) console.log(` ${dim(t('sync.secretsForced'))}`) + if (secrets.length > 0) {console.log(` ${dim(t('sync.secretsForced'))}`)} const remoteUrl = tryGit(['remote', 'get-url', 'origin'], cwd) const result = await pushReview({ record, remoteUrl, repoName: basename(cwd) }, creds) const doneKey = result.deduplicated ? 'sync.alreadySynced' : 'sync.pushed' @@ -332,7 +332,7 @@ export async function linkCommand(opts: { // Explicit pairing code (generated in the dashboard settings): direct link. if (opts.code) { const creds = loadSyncCredentials() - if (!creds) throw new Error(t('sync.noCredentials')) + if (!creds) {throw new Error(t('sync.noCredentials'))} const { tenant_id } = await linkWorkspace(opts.code, creds, fetchImpl) printOperationResult(t('sync.linked', { url: creds.url }), [{ label: t('field.account'), value: tenant_id }]) return diff --git a/packages/cli/src/tui.ts b/packages/cli/src/tui.ts index fc9a351..2227373 100644 --- a/packages/cli/src/tui.ts +++ b/packages/cli/src/tui.ts @@ -43,7 +43,7 @@ export async function select(opts: { /** false = erase the prompt entirely on resolve, leaving no trace (stable in-place menus). */ summary?: boolean }): Promise { - if (!isInteractive() || opts.options.length === 0) return null + if (!isInteractive() || opts.options.length === 0) {return null} const { stdin, stdout } = process emitKeypressEvents(stdin) @@ -59,19 +59,19 @@ export async function select(opts: { const width = () => Math.max(40, stdout.columns || 80) const filtered = (): SelectOption[] => { - if (!query) return opts.options + if (!query) {return opts.options} const q = query.toLowerCase() return opts.options.filter((o) => o.label.toLowerCase().includes(q)) } const clearRendered = () => { - if (renderedLines > 0) stdout.write(`\x1b[${renderedLines}A`) + if (renderedLines > 0) {stdout.write(`\x1b[${renderedLines}A`)} stdout.write('\x1b[0J') } const render = () => { const list = filtered() - if (cursor >= list.length) cursor = Math.max(0, list.length - 1) + if (cursor >= list.length) {cursor = Math.max(0, list.length - 1)} const lines: string[] = [] const filterPart = opts.filter @@ -88,17 +88,17 @@ export async function select(opts: { } else { const start = Math.min(Math.max(0, cursor - MAX_VISIBLE + 2), Math.max(0, list.length - MAX_VISIBLE)) const visible = list.slice(start, start + MAX_VISIBLE) - if (start > 0) lines.push(` ${faint(t('tui.moreUp', { n: start }))}`) + if (start > 0) {lines.push(` ${faint(t('tui.moreUp', { n: start }))}`)} visible.forEach((option, i) => { const index = start + i const active = index === cursor const label = truncate(option.label, Math.floor(width() * 0.5)) const hint = option.hint ? ` ${faint(truncate(option.hint, Math.floor(width() * 0.35)))}` : '' - if (option.separatorBefore && lines.at(-1) !== '') lines.push('') + if (option.separatorBefore && lines.at(-1) !== '') {lines.push('')} lines.push(active ? ` ${color('❯', ACCENT)} ${color(label, ACCENT)}${hint}` : ` ${label}${hint}`) }) const rest = list.length - start - visible.length - if (rest > 0) lines.push(` ${faint(t('tui.moreDown', { n: rest }))}`) + if (rest > 0) {lines.push(` ${faint(t('tui.moreDown', { n: rest }))}`)} } lines.push(` ${faint(opts.filter ? t('tui.keysWithFilter') : t('tui.keys'))}`) @@ -114,7 +114,7 @@ export async function select(opts: { stdin.pause() clearRendered() renderedLines = 0 - if (opts.summary !== false) stdout.write(`${summary}\n`) + if (opts.summary !== false) {stdout.write(`${summary}\n`)} stdout.write('\x1b[?25h') resolve(value) } @@ -122,7 +122,7 @@ export async function select(opts: { const confirm = () => { const list = filtered() const chosen = list[cursor] - if (!chosen) return + if (!chosen) {return} finish(chosen.value, ` ${color('✔', ACCENT)} ${opts.title} ${faint('·')} ${chosen.label}`) } @@ -137,8 +137,8 @@ export async function select(opts: { stdout.write('\x1b[?25h\n') process.exit(130) } - if (key.name === 'return' || key.name === 'enter') return confirm() - if (key.name === 'escape') return cancel() + if (key.name === 'return' || key.name === 'enter') {return confirm()} + if (key.name === 'escape') {return cancel()} if (key.name === 'up' || (key.ctrl && key.name === 'p') || (!opts.filter && key.name === 'k')) { cursor = list.length ? (cursor - 1 + list.length) % list.length : 0 return render() @@ -147,7 +147,7 @@ export async function select(opts: { cursor = list.length ? (cursor + 1) % list.length : 0 return render() } - if (!opts.filter && key.name === 'q') return cancel() + if (!opts.filter && key.name === 'q') {return cancel()} if (!opts.filter && char && /^[1-9]$/.test(char)) { const index = Number(char) - 1 if (index < list.length) { @@ -175,7 +175,7 @@ export async function select(opts: { } export async function textInput(opts: { title: string; placeholder?: string }): Promise { - if (!isInteractive()) return null + if (!isInteractive()) {return null} const rl = createInterface({ input: process.stdin, output: process.stdout }) try { const suffix = opts.placeholder ? ` ${faint(`(${opts.placeholder})`)}` : '' diff --git a/packages/cli/src/ui.test.ts b/packages/cli/src/ui.test.ts index 9438c2e..2897165 100644 --- a/packages/cli/src/ui.test.ts +++ b/packages/cli/src/ui.test.ts @@ -79,13 +79,14 @@ describe('renderFieldRows', () => { { label: 'branch', value: 'main' }, { label: 'changes', value: '3 files' }, ]) + // oxlint-disable-next-line no-control-regex -- strips ANSI color escapes const stripAnsi = (text: string) => text.replace(/\x1b\[[0-9;]*m/g, '') expect(stripAnsi(lines[0]!)).toBe(` ${'branch'.padEnd(10)}main`) expect(stripAnsi(lines[1]!)).toBe(` ${'changes'.padEnd(10)}3 files`) } finally { process.stdout.isTTY = originalIsTTY - if (originalNoColor === undefined) delete process.env.NO_COLOR - else process.env.NO_COLOR = originalNoColor + if (originalNoColor === undefined) {delete process.env.NO_COLOR} + else {process.env.NO_COLOR = originalNoColor} } }) }) diff --git a/packages/cli/src/ui.ts b/packages/cli/src/ui.ts index 45bd412..ca78a8f 100644 --- a/packages/cli/src/ui.ts +++ b/packages/cli/src/ui.ts @@ -30,6 +30,7 @@ export function underline(text: string): string { return isFancy() ? `\x1b[4m${text}\x1b[0m` : text } +// oxlint-disable-next-line no-control-regex -- deliberately matches ANSI color escapes const ANSI_PATTERN = /\x1b\[[0-9;]*m/g function visibleLength(text: string): number { @@ -62,23 +63,28 @@ const BANNER = [ ' ╚═════╝ ╚═════╝ ╚═════╝ ╚══════╝╚══════╝╚══════╝╚═╝ ╚═╝╚═╝ ╚═╝', ] +/** Clamped lookup: any index past the palette maps to its last (lightest) color. */ +function oceanColor(index: number): number { + return OCEAN[Math.min(index, OCEAN.length - 1)] ?? ACCENT +} + export function printBanner(): void { - if (!isFancy()) return + if (!isFancy()) {return} console.log('') - if ((process.stdout.columns ?? 80) < BANNER[0]!.length + 4) { + if ((process.stdout.columns ?? 80) < (BANNER[0] ?? '').length + 4) { console.log(` ${paint('◆', ACCENT)} ${bold('codesema')} ${dim(`v${VERSION}`)}`) console.log('') return } BANNER.forEach((line, i) => { const version = i === BANNER.length - 1 ? ` ${dim(`v${VERSION}`)}` : '' - console.log(' ' + paint(line, OCEAN[Math.min(1 + i, OCEAN.length - 1)]!) + version) + console.log(' ' + paint(line, oceanColor(1 + i)) + version) }) console.log('') } export function printUpdateNotice(latest: string | null): void { - if (!latest || !isNewerVersion(VERSION, latest)) return + if (!latest || !isNewerVersion(VERSION, latest)) {return} console.log(` ${paint(t('ui.updateAvailable', { current: VERSION, latest }), AMBER)} ${dim('npm i -g codesema@latest')}`) } @@ -103,13 +109,13 @@ function truncateStatus(text: string): string { export function progressLabel(partial: PartialReview): string | null { if (partial.stepTitles.length > 0) { - const current = partial.stepTitles[partial.stepTitles.length - 1]! + const current = partial.stepTitles.at(-1) ?? '' return truncateStatus(t('ui.progressStep', { n: partial.stepTitles.length, title: current })) } if (partial.findings.length > 0) { return t('ui.progressFindings', { n: partial.findings.length }) } - if (partial.verdict) return t('ui.progressVerdict', { verdict: partial.verdict }) + if (partial.verdict) {return t('ui.progressVerdict', { verdict: partial.verdict })} return null } @@ -154,11 +160,11 @@ export function startSpinner(label: string): Spinner { for (let i = 0; i < WAVE_WIDTH; i++) { const level = (Math.sin(i * 0.55 - tick * 0.18) + 1) / 2 const h = Math.round(level * (WAVE_CHARS.length - 1)) - const color = OCEAN[Math.min(Math.round(level * (OCEAN.length - 1)), OCEAN.length - 1)]! - wave += paint(WAVE_CHARS[h]!, color) + const color = oceanColor(Math.round(level * (OCEAN.length - 1))) + wave += paint(WAVE_CHARS.charAt(h), color) } const secs = Math.floor((Date.now() - startedAt) / 1000) - const status = liveStatus ?? t(PHASE_KEYS[Math.floor(secs / 7) % PHASE_KEYS.length]!) + const status = liveStatus ?? t(PHASE_KEYS[Math.floor(secs / 7) % PHASE_KEYS.length] ?? PHASE_KEYS[0]) process.stdout.write(`\r\x1b[2K ${wave} ${label} ${dim(`${elapsed(startedAt)} · ${status}`)}`) } diff --git a/packages/cli/src/version.ts b/packages/cli/src/version.ts index a73909e..33332e1 100644 --- a/packages/cli/src/version.ts +++ b/packages/cli/src/version.ts @@ -6,16 +6,14 @@ export const VERSION = typeof __CODESEMA_VERSION__ !== 'undefined' ? __CODESEMA_ /** Numeric x.y.z comparison; a leading "v" and any prerelease suffix are ignored. */ export function isNewerVersion(current: string, latest: string): boolean { const parse = (v: string) => - v - .replace(/^v/, '') - .split('-')[0]! + (v.replace(/^v/, '').split('-')[0] ?? '') .split('.') .map((part) => Number.parseInt(part, 10) || 0) const a = parse(current) const b = parse(latest) for (let i = 0; i < 3; i++) { const diff = (b[i] ?? 0) - (a[i] ?? 0) - if (diff !== 0) return diff > 0 + if (diff !== 0) {return diff > 0} } return false } @@ -25,12 +23,12 @@ export function isNewerVersion(current: string, latest: string): boolean { * Best-effort: returns null when offline, slow, or opted out via CODESEMA_NO_UPDATE_CHECK. */ export function startUpdateCheck(): Promise { - if (process.env.CODESEMA_NO_UPDATE_CHECK || !process.stdout.isTTY) return Promise.resolve(null) + if (process.env.CODESEMA_NO_UPDATE_CHECK || !process.stdout.isTTY) {return Promise.resolve(null)} return fetch('https://registry.npmjs.org/-/package/codesema/dist-tags', { signal: AbortSignal.timeout(2500), }) .then(async (res) => { - if (!res.ok) return null + if (!res.ok) {return null} const tags = (await res.json()) as { latest?: unknown } return typeof tags.latest === 'string' ? tags.latest : null }) diff --git a/packages/cli/src/wizard.ts b/packages/cli/src/wizard.ts index a71a3c2..3d6f728 100644 --- a/packages/cli/src/wizard.ts +++ b/packages/cli/src/wizard.ts @@ -88,9 +88,9 @@ export type WizardResult = { export function composeCommand(def: AgentDef, model?: string, effort?: string): string { let command = def.base - if (model) command += ` ${def.modelFlag} ${model}` - if (effort && def.effortFlag) command += ` ${def.effortFlag(effort)}` - if (def.suffix) command += ` ${def.suffix}` + if (model) {command += ` ${def.modelFlag} ${model}`} + if (effort && def.effortFlag) {command += ` ${def.effortFlag(effort)}`} + if (def.suffix) {command += ` ${def.suffix}`} return command } @@ -101,8 +101,8 @@ const CUSTOM = Symbol('custom') const LANGUAGE_TITLE = 'Language / Langue?' function defaultLanguageIndex(current?: SupportedLanguage): number { - if (current === 'en') return 0 - if (current === 'fr') return 1 + if (current === 'en') {return 0} + if (current === 'fr') {return 1} const env = process.env.LC_ALL || process.env.LC_MESSAGES || process.env.LANG || '' return env.toLowerCase().startsWith('fr') ? 1 : 0 } @@ -121,7 +121,7 @@ export async function pickLanguage(current?: SupportedLanguage): Promise { - if (!isInteractive()) return null + if (!isInteractive()) {return null} const detected = detectAgents(cwd) const missing = AGENT_DEFS.filter((d) => !detected.includes(d)) @@ -147,7 +147,7 @@ export async function runAgentWizard(cwd: string, current: CodesemaConfig = {}): options: agentOptions, initialIndex: initialAgent >= 0 ? initialAgent : 0, }) - if (picked === null) return null + if (picked === null) {return null} if (picked === CUSTOM) { const command = await textInput({ @@ -174,7 +174,7 @@ export async function runAgentWizard(cwd: string, current: CodesemaConfig = {}): options: modelOptions, initialIndex: initialModel >= 0 ? initialModel : 0, }) - if (modelPick === null) return null + if (modelPick === null) {return null} let model: string | undefined if (modelPick === CUSTOM) { model = (await textInput({ title: t('wizard.modelName') })) ?? undefined @@ -198,8 +198,8 @@ export async function runAgentWizard(cwd: string, current: CodesemaConfig = {}): options: effortOptions, initialIndex: initialEffort >= 0 ? initialEffort : effortOptions.length - 1, }) - if (effortPick === null) return null - if (effortPick !== CLI_DEFAULT) effort = effortPick + if (effortPick === null) {return null} + if (effortPick !== CLI_DEFAULT) {effort = effortPick} } return { command: composeCommand(def, model, effort), agentId: def.id, model, effort } @@ -209,8 +209,8 @@ function applyResult(config: CodesemaConfig, result: WizardResult): CodesemaConf const next: CodesemaConfig = { ...config, agent: result.command, agentId: result.agentId } delete next.model delete next.effort - if (result.model) next.model = result.model - if (result.effort) next.effort = result.effort + if (result.model) {next.model = result.model} + if (result.effort) {next.effort = result.effort} return next } @@ -220,14 +220,14 @@ function applyResult(config: CodesemaConfig, result: WizardResult): CodesemaConf */ export async function runOnboarding(cwd: string): Promise { const language = (await pickLanguage()) ?? undefined - if (language) setLanguage(language) + if (language) {setLanguage(language)} console.log(` ${bold(t('wizard.firstRun'))}`) console.log(` ${dim(t('wizard.firstRunHint'))}`) console.log('') const result = await runAgentWizard(cwd) - if (!result) return null + if (!result) {return null} const config = applyResult(loadGlobalConfig(), result) - if (language) config.language = language + if (language) {config.language = language} const path = saveGlobalConfig(config) console.log(` ${dim(t('wizard.saved', { path }))}`) return result.command @@ -242,13 +242,13 @@ export type ConfigEntry = { } function languageLabel(language?: SupportedLanguage): string { - if (language === 'en') return 'English' - if (language === 'fr') return 'Français' + if (language === 'en') {return 'English'} + if (language === 'fr') {return 'Français'} return t('config.languageAuto') } function autoSyncLabel(syncAutoPush: boolean | undefined): string { - if (syncAutoPush === undefined) return t('config.autoSyncUnset') + if (syncAutoPush === undefined) {return t('config.autoSyncUnset')} return syncAutoPush ? t('config.autoSyncOn') : t('config.autoSyncOff') } @@ -283,11 +283,11 @@ export async function configCommand(repoRoot: string | null): Promise { })), summary: false, }) - if (picked === null || picked === 'back') return + if (picked === null || picked === 'back') {return} if (picked === 'language') { const language = await pickLanguage(current.language) - if (!language) continue + if (!language) {continue} setLanguage(language) // The UI language is global by nature; a per-repo override remains possible // by hand in .codesema/config.json but is not offered here. @@ -308,7 +308,7 @@ export async function configCommand(repoRoot: string | null): Promise { initialIndex: current.syncAutoPush === true ? 1 : 0, summary: false, }) - if (choice === null) continue + if (choice === null) {continue} // Auto-sync is global-only, like the sync credentials it depends on: a // repo config must never be able to turn on pushing diffs off the machine. const path = saveGlobalConfig({ ...loadGlobalConfig(), syncAutoPush: choice === 'on' }) @@ -332,7 +332,7 @@ async function configureAgent(repoRoot: string | null, current: CodesemaConfig): } const result = await runAgentWizard(repoRoot ?? process.cwd(), current) - if (!result) return + if (!result) {return} let scope: 'global' | 'repo' = 'global' if (repoRoot) { @@ -343,7 +343,7 @@ async function configureAgent(repoRoot: string | null, current: CodesemaConfig): { label: t('config.thisRepo'), hint: t('config.thisRepoHint'), value: 'repo' }, ], }) - if (pickedScope === null) return + if (pickedScope === null) {return} scope = pickedScope } diff --git a/packages/contract/src/index.test.ts b/packages/contract/src/index.test.ts index 51de60a..d6f3ffd 100644 --- a/packages/contract/src/index.test.ts +++ b/packages/contract/src/index.test.ts @@ -435,20 +435,20 @@ describe('reviewRecordSchema', () => { test('every $ref resolves to a defined $def', () => { const refs: string[] = [] const walk = (node: unknown): void => { - if (!node || typeof node !== 'object') return + if (!node || typeof node !== 'object') {return} for (const [key, value] of Object.entries(node)) { - if (key === '$ref' && typeof value === 'string') refs.push(value) - else walk(value) + if (key === '$ref' && typeof value === 'string') {refs.push(value)} + else {walk(value)} } } walk(reviewRecordSchema) const defs = new Set(Object.keys(reviewRecordSchema.$defs)) expect(refs.length).toBeGreaterThan(0) - for (const ref of refs) expect(defs.has(ref.replace('#/$defs/', ''))).toBe(true) + for (const ref of refs) {expect(defs.has(ref.replace('#/$defs/', ''))).toBe(true)} }) test('top-level required keys all exist in properties', () => { const props = new Set(Object.keys(reviewRecordSchema.properties)) - for (const key of reviewRecordSchema.required) expect(props.has(key)).toBe(true) + for (const key of reviewRecordSchema.required) {expect(props.has(key)).toBe(true)} }) }) diff --git a/packages/contract/src/index.ts b/packages/contract/src/index.ts index 4539b14..231b94f 100644 --- a/packages/contract/src/index.ts +++ b/packages/contract/src/index.ts @@ -103,14 +103,14 @@ const MESSAGE_MAX = 2000 const SUGGESTION_MAX = 4000 function sanitizeReviewFirst(raw: unknown, stepsCount: number): ReviewFirstItem[] { - if (!Array.isArray(raw)) return [] + if (!Array.isArray(raw)) {return []} const out: ReviewFirstItem[] = [] for (const item of raw) { - if (out.length >= REVIEW_FIRST_MAX) break - if (!item || typeof item !== 'object') continue + if (out.length >= REVIEW_FIRST_MAX) {break} + if (!item || typeof item !== 'object') {continue} const it = item as Record const point = typeof it.point === 'string' ? it.point.trim().slice(0, REVIEW_FIRST_POINT_MAX) : '' - if (!point) continue + if (!point) {continue} const risk: ReviewFirstRisk = it.risk === 'high' || it.risk === 'low' ? it.risk : 'medium' // Archives written before the step rename used "chapter_ref". const rawRef = it.step_ref ?? it.chapter_ref @@ -125,25 +125,25 @@ function sanitizeReviewFirst(raw: unknown, stepsCount: number): ReviewFirstItem[ } function sanitizeRisk(raw: unknown): NarrativeRisk | undefined { - if (raw === 'high' || raw === 'medium' || raw === 'low') return raw + if (raw === 'high' || raw === 'medium' || raw === 'low') {return raw} return undefined } function sanitizePrologue(raw: unknown): NarrativePrologue | undefined { - if (!raw || typeof raw !== 'object') return undefined + if (!raw || typeof raw !== 'object') {return undefined} const p = raw as Record const why = typeof p.why === 'string' ? p.why.trim() : '' const what = typeof p.what === 'string' ? p.what.trim() : '' - if (!why && !what) return undefined + if (!why && !what) {return undefined} const key_changes: NarrativePrologueKeyChange[] = [] if (Array.isArray(p.key_changes)) { for (const item of p.key_changes) { - if (key_changes.length >= KEY_CHANGES_MAX) break - if (!item || typeof item !== 'object') continue + if (key_changes.length >= KEY_CHANGES_MAX) {break} + if (!item || typeof item !== 'object') {continue} const it = item as Record const title = typeof it.title === 'string' ? it.title.trim() : '' const detail = typeof it.detail === 'string' ? it.detail.trim() : '' - if (!title) continue + if (!title) {continue} key_changes.push({ title, detail }) } } @@ -151,7 +151,7 @@ function sanitizePrologue(raw: unknown): NarrativePrologue | undefined { } export function sanitizeNarrative(raw: unknown, findingsCount: number): ReviewNarrative | null { - if (!raw || typeof raw !== 'object') return null + if (!raw || typeof raw !== 'object') {return null} const r = raw as Record const intent = typeof r.intent === 'string' ? r.intent.trim() : '' @@ -162,10 +162,10 @@ export function sanitizeNarrative(raw: unknown, findingsCount: number): ReviewNa const steps: NarrativeStep[] = [] for (const c of rawSteps) { - if (!c || typeof c !== 'object') continue + if (!c || typeof c !== 'object') {continue} const cc = c as Record const title = typeof cc.title === 'string' ? cc.title.trim() : '' - if (!title) continue + if (!title) {continue} const rationale = typeof cc.rationale === 'string' ? cc.rationale.trim() : '' const files = Array.isArray(cc.files) ? cc.files.filter((f): f is string => typeof f === 'string').map((f) => f.slice(0, FILE_MAX)) @@ -197,30 +197,30 @@ export function sanitizeNarrative(raw: unknown, findingsCount: number): ReviewNa }) } - if (steps.length === 0 && !intent) return null + if (steps.length === 0 && !intent) {return null} const review_first = sanitizeReviewFirst(r.review_first, steps.length) return { intent, confidence, ...(prologue ? { prologue } : {}), steps, review_first } } -const SEVERITIES: readonly FindingSeverity[] = ['critical', 'major', 'minor', 'info'] -const KINDS: readonly FindingKind[] = ['security', 'perf', 'convention', 'design', 'praise', 'why'] +const SEVERITIES: ReadonlySet = new Set(['critical', 'major', 'minor', 'info']) +const KINDS: ReadonlySet = new Set(['security', 'perf', 'convention', 'design', 'praise', 'why']) export function sanitizeFindings(raw: unknown): Finding[] { - if (!Array.isArray(raw)) return [] + if (!Array.isArray(raw)) {return []} const out: Finding[] = [] for (const item of raw) { - if (!item || typeof item !== 'object') continue + if (!item || typeof item !== 'object') {continue} const f = item as Record const file = typeof f.file === 'string' ? f.file.trim().slice(0, FILE_MAX) : '' const message = typeof f.message === 'string' ? f.message.trim().slice(0, MESSAGE_MAX) : '' - if (!file || !message) continue - const kind = KINDS.includes(f.kind as FindingKind) ? (f.kind as FindingKind) : undefined + if (!file || !message) {continue} + const kind = KINDS.has(f.kind as FindingKind) ? (f.kind as FindingKind) : undefined // A praise/why finding carries no defect: any higher severity would trip // the verdict escalation and the --fail-on gate. const severity: FindingSeverity = kind === 'praise' || kind === 'why' ? 'info' - : SEVERITIES.includes(f.severity as FindingSeverity) + : SEVERITIES.has(f.severity as FindingSeverity) ? (f.severity as FindingSeverity) : 'info' const line = Number.isInteger(f.line) && (f.line as number) > 0 ? (f.line as number) : undefined @@ -249,14 +249,14 @@ export function sanitizeFindings(raw: unknown): Finding[] { const FILES_REVIEWED_MAX = 500 function sanitizeFilesReviewed(raw: unknown): string[] | undefined { - if (!Array.isArray(raw)) return undefined + if (!Array.isArray(raw)) {return undefined} const seen = new Set() for (const item of raw) { - if (typeof item !== 'string') continue + if (typeof item !== 'string') {continue} const path = item.trim().slice(0, FILE_MAX) - if (!path) continue + if (!path) {continue} seen.add(path) - if (seen.size >= FILES_REVIEWED_MAX) break + if (seen.size >= FILES_REVIEWED_MAX) {break} } return [...seen] } @@ -278,15 +278,15 @@ export function sanitizeReview(raw: unknown): SanitizedReview { * a usable object; shape fields are normalized. */ function sanitizeDualStats(raw: unknown): DualStats | undefined { - if (!raw || typeof raw !== 'object') return undefined + if (!raw || typeof raw !== 'object') {return undefined} const d = raw as Record const counts = [d.merged, d.rejected, d.added_by_b] - if (!counts.every((n) => Number.isInteger(n) && (n as number) >= 0)) return undefined + if (!counts.every((n) => Number.isInteger(n) && (n as number) >= 0)) {return undefined} return { merged: d.merged as number, rejected: d.rejected as number, added_by_b: d.added_by_b as number } } export function sanitizeRecord(raw: unknown): ReviewRecord | null { - if (!raw || typeof raw !== 'object') return null + if (!raw || typeof raw !== 'object') {return null} const r = raw as Record const m = (r.meta && typeof r.meta === 'object' ? r.meta : {}) as Record const str = (v: unknown): string => (typeof v === 'string' ? v : '') @@ -338,10 +338,10 @@ const CONTENT_PATTERNS: readonly { label: string; re: RegExp }[] = [ function sensitiveFilename(path: string): boolean { const base = (path.split('/').pop() ?? '').toLowerCase() - if (!base) return false - if (SENSITIVE_BASENAMES.has(base)) return true - if (base === '.env') return true - if (base.startsWith('.env.')) return !DOTENV_ALLOWED_SUFFIXES.has(base.slice(5)) + if (!base) {return false} + if (SENSITIVE_BASENAMES.has(base)) {return true} + if (base === '.env') {return true} + if (base.startsWith('.env.')) {return !DOTENV_ALLOWED_SUFFIXES.has(base.slice(5))} const dot = base.lastIndexOf('.') return dot > 0 && SENSITIVE_EXTENSIONS.has(base.slice(dot + 1)) } @@ -356,7 +356,7 @@ function markerLinePath(line: string): string { const rest = line.slice(4) const tab = rest.indexOf('\t') const raw = (tab === -1 ? rest : rest.slice(0, tab)).trim() - if (raw === '/dev/null') return '' + if (raw === '/dev/null') {return ''} return raw.startsWith('a/') || raw.startsWith('b/') ? raw.slice(2) : raw } @@ -367,12 +367,12 @@ function markerLinePath(line: string): string { * whether to hold the diff back. */ export function detectDiffSecrets(diff: string): SecretMatch[] { - if (typeof diff !== 'string' || !diff) return [] + if (typeof diff !== 'string' || !diff) {return []} const matches: SecretMatch[] = [] const seen = new Set() const add = (file: string, reason: SecretMatchReason, detail: string): void => { const key = `${file}\0${reason}\0${detail}` - if (seen.has(key)) return + if (seen.has(key)) {return} seen.add(key) matches.push({ file, reason, detail }) } @@ -380,23 +380,23 @@ export function detectDiffSecrets(diff: string): SecretMatch[] { for (const line of diff.split('\n')) { if (line.startsWith('diff --git ')) { currentFile = gitHeaderNewPath(line) - if (currentFile && sensitiveFilename(currentFile)) add(currentFile, 'filename', currentFile) + if (currentFile && sensitiveFilename(currentFile)) {add(currentFile, 'filename', currentFile)} continue } if (line.startsWith('+++ ') || line.startsWith('--- ')) { const path = markerLinePath(line) if (path) { - if (!currentFile) currentFile = path - if (sensitiveFilename(path)) add(path, 'filename', path) + if (!currentFile) {currentFile = path} + if (sensitiveFilename(path)) {add(path, 'filename', path)} } continue } const isAdded = line.startsWith('+') && !line.startsWith('+++') const isRemoved = line.startsWith('-') && !line.startsWith('---') - if (!isAdded && !isRemoved) continue + if (!isAdded && !isRemoved) {continue} const content = line.slice(1) for (const { label, re } of CONTENT_PATTERNS) { - if (re.test(content)) add(currentFile || '(unknown file)', 'content', label) + if (re.test(content)) {add(currentFile || '(unknown file)', 'content', label)} } } return matches @@ -421,13 +421,13 @@ function indexDiff(diff: string): DiffIndex | null { if (line.startsWith('diff --git ')) { currentNewPath = '' const path = gitHeaderNewPath(line) - if (path) files.add(path) + if (path) {files.add(path)} continue } if (line.startsWith('--- ') || line.startsWith('+++ ')) { const path = markerLinePath(line) - if (path) files.add(path) - if (line.startsWith('+++ ')) currentNewPath = path + if (path) {files.add(path)} + if (line.startsWith('+++ ')) {currentNewPath = path} continue } const hunk = /^@@ -\d+(?:,\d+)? \+(\d+)(?:,(\d+))? @@/.exec(line) @@ -464,7 +464,7 @@ export function groundReview( ): { review: SanitizedReview; report: GroundingReport } { const report: GroundingReport = { dropped: [], deanchored: [], merged: 0, verdict_escalated: false } const index = typeof diff === 'string' ? indexDiff(diff) : null - if (!index) return { review, report } + if (!index) {return { review, report }} const newIndexByOld = new Map() const keptIndexByKey = new Map() diff --git a/packages/web/src/App.vue b/packages/web/src/App.vue index f102d2d..c9b01eb 100644 --- a/packages/web/src/App.vue +++ b/packages/web/src/App.vue @@ -16,8 +16,8 @@ let events: EventSource | null = null async function loadRecord(): Promise { const res = await fetch('/api/review') - if (res.status === 202) return false - if (!res.ok) throw new Error(`HTTP ${res.status}`) + if (res.status === 202) {return false} + if (!res.ok) {throw new Error(`HTTP ${res.status}`)} record.value = (await res.json()) as ReviewRecord return true } @@ -55,7 +55,7 @@ function openEvents() { async function load() { error.value = null try { - if (await loadRecord()) return + if (await loadRecord()) {return} openEvents() } catch (e) { error.value = e instanceof Error ? e.message : String(e) diff --git a/packages/web/src/components/DiffView.vue b/packages/web/src/components/DiffView.vue index bd20c2a..a94709f 100644 --- a/packages/web/src/components/DiffView.vue +++ b/packages/web/src/components/DiffView.vue @@ -21,7 +21,7 @@ const isClient = typeof window !== 'undefined' const SPLIT_KEY = 'codesema-diff-mode' function loadMode(): 'split' | 'unified' { - if (!isClient) return 'unified' + if (!isClient) {return 'unified'} return (localStorage.getItem(SPLIT_KEY) as 'split' | 'unified') ?? 'unified' } @@ -31,14 +31,14 @@ const diffMode = computed<'split' | 'unified'>(() => props.mode ?? internalMode. function setMode(m: 'split' | 'unified') { internalMode.value = m - if (isClient) localStorage.setItem(SPLIT_KEY, m) + if (isClient) {localStorage.setItem(SPLIT_KEY, m)} } // Large files (or files past the page's cumulative budget) start collapsed: their // DOM (v-if) is only created on expand, keeping the first render smooth on huge diffs. function initialCollapsedSet(): Set { const collapsed = collapsedByBudget(props.files) - if (props.initialCollapsed) for (const f of props.files) collapsed.add(f.path) + if (props.initialCollapsed) {for (const f of props.files) {collapsed.add(f.path)}} return collapsed } @@ -48,14 +48,14 @@ const collapsed = ref>(initialCollapsedSet()) watch( () => props.collapseKey, (k) => { - if (k == null) return + if (k == null) {return} collapsed.value = k % 2 === 1 ? new Set(props.files.map((f) => f.path)) : new Set() }, ) function toggleFile(path: string) { - if (collapsed.value.has(path)) collapsed.value.delete(path) - else collapsed.value.add(path) + if (collapsed.value.has(path)) {collapsed.value.delete(path)} + else {collapsed.value.add(path)} // force reactivity collapsed.value = new Set(collapsed.value) } @@ -91,7 +91,7 @@ const FALLBACK_KIND: KindMeta = { label: t('diffView.sevInfo'), color: 'var(--co function resolveKind(f: Finding): KindMeta { if (f.kind) { const k = NL_KIND[f.kind] - if (k) return k + if (k) {return k} } return SEV_KIND[f.severity] ?? FALLBACK_KIND } @@ -111,10 +111,10 @@ function splitRows(rows: HunkLine[]): SplitRow[] { return toSplit(rows) } -function cellClass(t: 'add' | 'del' | 'ctx' | 'nil'): string { - if (t === 'add') return 'srd-cell-add' - if (t === 'del') return 'srd-cell-del' - if (t === 'nil') return 'srd-cell-nil' +function cellClass(kind: 'add' | 'del' | 'ctx' | 'nil'): string { + if (kind === 'add') {return 'srd-cell-add'} + if (kind === 'del') {return 'srd-cell-del'} + if (kind === 'nil') {return 'srd-cell-nil'} return 'srd-cell-ctx' } @@ -135,7 +135,7 @@ function hunkRows(block: HunkBlock): HunkLine[] { } function extraNotes(byLine: Record, lineNo: number | null): Finding[] { - if (lineNo == null) return [] + if (lineNo == null) {return []} return (byLine[lineNo] ?? []).slice(1) } @@ -157,7 +157,7 @@ async function revealFinding(id: number): Promise { } await nextTick() const anchor = rootEl.value?.querySelector(`[data-finding-id="${id}"]`) - if (!(anchor instanceof HTMLElement)) return + if (!(anchor instanceof HTMLElement)) {return} anchor.scrollIntoView({ behavior: 'smooth', block: 'center' }) const card = anchor.closest('.nlr-note') ?? anchor card.classList.add('nlr-note--flash') @@ -167,7 +167,7 @@ async function revealFinding(id: number): Promise { watch( () => props.reveal, (r) => { - if (r) void revealFinding(r.id) + if (r) {void revealFinding(r.id)} }, ) diff --git a/packages/web/src/components/DualJudgePanel.vue b/packages/web/src/components/DualJudgePanel.vue index 0dc5eaa..fa4fdba 100644 --- a/packages/web/src/components/DualJudgePanel.vue +++ b/packages/web/src/components/DualJudgePanel.vue @@ -21,7 +21,7 @@ const STAMP_LABEL_KEY: Record = { } function stampFor(d: JudgeDecision): Stamp { - if (d.duplicate_of) return 'merged' + if (d.duplicate_of) {return 'merged'} return d.action === 'reject' ? 'rejected' : 'kept' } @@ -34,7 +34,7 @@ const done = computed(() => props.judge?.decisions.length ?? 0) const pct = computed(() => (total.value > 0 ? Math.round((done.value / total.value) * 100) : 0)) // Newest decision first: the judge appends to the cumulative list as it resolves each one. -const reversedDecisions = computed(() => [...(props.judge?.decisions ?? [])].reverse()) +const reversedDecisions = computed(() => [...(props.judge?.decisions ?? [])].toReversed())