From a42988a3e236d12ef5bc09887f586d3fa41ad161 Mon Sep 17 00:00:00 2001 From: Brad Groux <3053586+BradGroux@users.noreply.github.com> Date: Thu, 4 Jun 2026 00:16:42 -0700 Subject: [PATCH] Ratcheting lint warning budget --- cli/src/commands/agents.ts | 1 - cli/src/commands/time.ts | 1 - cli/src/commands/usage.ts | 2 +- docs/CODEBASE-AUDIT-2026-05-16.md | 4 +- docs/testing/lint-warning-debt.md | 69 +++++++ package.json | 3 +- scripts/lint-warning-budget.mjs | 192 ++++++++++++++++++ server/src/middleware/auth.ts | 4 +- server/src/middleware/validate.ts | 2 +- server/src/middleware/workflow-auth.ts | 2 +- server/src/routes/agent-status.ts | 2 - server/src/routes/agents.ts | 6 +- server/src/routes/chat.ts | 2 +- server/src/routes/feedback.ts | 11 +- server/src/routes/integrations.ts | 2 +- server/src/routes/metrics.ts | 3 +- server/src/routes/projects.ts | 1 - server/src/routes/sprints.ts | 1 - server/src/routes/status-history.ts | 1 - server/src/routes/task-archive.ts | 2 +- server/src/routes/task-observations.ts | 2 +- server/src/routes/task-subtasks.ts | 4 - server/src/routes/task-types.ts | 1 - server/src/services/analytics-service.ts | 9 +- server/src/services/attachment-service.ts | 1 - server/src/services/broadcast-service.ts | 2 +- server/src/services/changes-service.ts | 8 +- server/src/services/chat-service.ts | 2 - server/src/services/clawdbot-agent-service.ts | 1 - .../src/services/cost-prediction-service.ts | 24 ++- server/src/services/diff-service.ts | 1 - server/src/services/digest-service.ts | 10 +- server/src/services/error-learning-service.ts | 8 +- server/src/services/hook-service.ts | 2 +- server/src/services/metrics/helpers.ts | 1 - .../src/services/metrics/metrics-service.ts | 13 +- server/src/services/metrics/task-metrics.ts | 1 - server/src/services/project-service.ts | 3 - .../src/services/prompt-registry-service.ts | 1 - server/src/services/sprint-service.ts | 3 - server/src/services/task-service.ts | 1 - .../src/services/text-extraction-service.ts | 7 +- server/src/services/worktree-service.ts | 8 +- web/src/hooks/useToast.tsx | 15 +- web/src/lib/template-io.ts | 4 +- 45 files changed, 331 insertions(+), 112 deletions(-) create mode 100644 docs/testing/lint-warning-debt.md create mode 100644 scripts/lint-warning-budget.mjs diff --git a/cli/src/commands/agents.ts b/cli/src/commands/agents.ts index 8a0521a2..fd2f0b0c 100644 --- a/cli/src/commands/agents.ts +++ b/cli/src/commands/agents.ts @@ -2,7 +2,6 @@ import { Command } from 'commander'; import chalk from 'chalk'; import { api } from '../utils/api.js'; import { findTask } from '../utils/find.js'; -import type { Task } from '../utils/types.js'; export function registerAgentCommands(program: Command): void { // Start agent on task diff --git a/cli/src/commands/time.ts b/cli/src/commands/time.ts index b35329e8..b966fb0c 100644 --- a/cli/src/commands/time.ts +++ b/cli/src/commands/time.ts @@ -2,7 +2,6 @@ import { Command } from 'commander'; import chalk from 'chalk'; import { api } from '../utils/api.js'; import { findTask } from '../utils/find.js'; -import type { Task } from '../utils/types.js'; function formatDuration(totalSeconds: number): string { const hours = Math.floor(totalSeconds / 3600); diff --git a/cli/src/commands/usage.ts b/cli/src/commands/usage.ts index ef50fa8b..a5f56b53 100644 --- a/cli/src/commands/usage.ts +++ b/cli/src/commands/usage.ts @@ -250,7 +250,7 @@ async function displayTaskUsage(taskId: string, period: string, json: boolean): } export function registerUsageCommands(program: Command): void { - const usage = program + program .command('usage') .description('Display usage statistics (tokens, costs, time)') .option( diff --git a/docs/CODEBASE-AUDIT-2026-05-16.md b/docs/CODEBASE-AUDIT-2026-05-16.md index 98e22a37..fa9b5583 100644 --- a/docs/CODEBASE-AUDIT-2026-05-16.md +++ b/docs/CODEBASE-AUDIT-2026-05-16.md @@ -31,8 +31,8 @@ This audit covered the server, web app, shared package, CLI, MCP server, docs, s - `pnpm test:unit` passes across server and web packages. - Targeted regressions pass for URL validation, squad webhooks, task checkpoint/dependency behavior, auth, feedback, and web API helpers. - `pnpm build` passes across shared, server, web, CLI, and MCP after expanding the root build. -- `pnpm lint` exits 0 and currently reports 714 warnings, down from 728, mostly `any`, non-null assertions, and hook dependency warnings. -- `pnpm lint:budget` enforces the current warning ceiling so future cleanup can ratchet it down. +- `pnpm lint` exits 0 and currently reports 601 warnings, down from 728, mostly `any`, non-null assertions, and test-only unused values. +- `pnpm lint:budget` enforces the current warning ceiling and prints package/rule counts so future cleanup can ratchet it down. - `pnpm validate:release -- --github` passes for v4.3.1, including local tag, origin tag, and published GitHub release checks. - Scheduled QA workflow YAML parses successfully. - The Vite production build no longer emits oversized chunk warnings; the largest app chunk is the lazy `TaskDetailPanel` chunk at 473.98 kB. diff --git a/docs/testing/lint-warning-debt.md b/docs/testing/lint-warning-debt.md new file mode 100644 index 00000000..53760f35 --- /dev/null +++ b/docs/testing/lint-warning-debt.md @@ -0,0 +1,69 @@ +# Lint Warning Debt + +Review date: 2026-06-04 + +The repository still allows lint warnings, but warning debt is now managed with a +ratchetable budget and a repeatable package/rule report. + +## Commands + +Plain lint remains available for the full ESLint output: + +```bash +pnpm lint +``` + +The CI gate runs: + +```bash +pnpm lint:budget +``` + +The budget script runs ESLint in JSON mode, prints counts by package, rule, and +package/rule pair, then fails when warnings exceed the configured ceiling. + +For a local report without enforcing the ceiling: + +```bash +pnpm lint:report +``` + +## Current Budget + +Current warning budget: 601. + +Baseline after the production unused-value cleanup: + +| Package | Warnings | +| ------- | -------- | +| server | 536 | +| web | 38 | +| mcp | 25 | +| shared | 2 | + +Current warning classes: + +| Rule | Warnings | +| ------------------------------------------ | -------- | +| `@typescript-eslint/no-explicit-any` | 342 | +| `@typescript-eslint/no-non-null-assertion` | 227 | +| `@typescript-eslint/no-unused-vars` | 31 | +| `react-hooks/exhaustive-deps` | 1 | + +## Cleanup Order + +1. Production code before test fixtures. +2. Unused values before type-shape cleanups. +3. `no-explicit-any` in API boundaries and storage repositories before broad + test mocks. +4. Non-null assertions in runtime paths before test setup helpers. +5. React hook dependency fixes only when the behavior is understood and covered. + +Each cleanup PR should lower `lint:budget` to the new observed count. Do not +relax rules or add ignore blocks just to hide the backlog. + +## Touched Code Rule + +When editing a file with existing warning debt, avoid adding new warnings in +that file. If a warning is directly adjacent to the change and cheap to fix, +fix it and ratchet the budget in the same PR. diff --git a/package.json b/package.json index fb177c5d..1771c5df 100644 --- a/package.json +++ b/package.json @@ -24,7 +24,8 @@ "dev:watchdog": "bash scripts/dev-watchdog.sh", "build": "pnpm --filter @veritas-kanban/shared build && pnpm --filter @veritas-kanban/server build && pnpm --filter @veritas-kanban/web build && pnpm --filter @veritas-kanban/cli build && pnpm --filter @veritas-kanban/mcp build && pnpm --filter @veritas-kanban/desktop build", "lint": "eslint .", - "lint:budget": "eslint . --max-warnings=714", + "lint:budget": "node scripts/lint-warning-budget.mjs --max-warnings=601", + "lint:report": "node scripts/lint-warning-budget.mjs --all-rules", "lint:fix": "eslint . --fix", "typecheck": "pnpm --filter @veritas-kanban/shared build && pnpm -r typecheck", "test": "vitest run", diff --git a/scripts/lint-warning-budget.mjs b/scripts/lint-warning-budget.mjs new file mode 100644 index 00000000..aa551ec3 --- /dev/null +++ b/scripts/lint-warning-budget.mjs @@ -0,0 +1,192 @@ +#!/usr/bin/env node + +import { spawnSync } from 'node:child_process'; +import path from 'node:path'; + +const KNOWN_PACKAGES = new Set(['cli', 'desktop', 'mcp', 'server', 'shared', 'web']); +const repoRoot = process.cwd(); + +function parseArgs(argv) { + const eslintArgs = []; + let maxWarnings = null; + let showAllRules = false; + + for (let index = 0; index < argv.length; index += 1) { + const arg = argv[index]; + if (arg === '--max-warnings') { + const value = argv[index + 1]; + if (!value) throw new Error('--max-warnings requires a numeric value'); + maxWarnings = Number(value); + index += 1; + continue; + } + if (arg.startsWith('--max-warnings=')) { + maxWarnings = Number(arg.slice('--max-warnings='.length)); + continue; + } + if (arg === '--all-rules') { + showAllRules = true; + continue; + } + eslintArgs.push(arg); + } + + if (maxWarnings !== null && !Number.isInteger(maxWarnings)) { + throw new Error('--max-warnings must be an integer'); + } + + return { + eslintArgs: eslintArgs.length > 0 ? eslintArgs : ['.'], + maxWarnings, + showAllRules, + }; +} + +function relativeFile(filePath) { + const relative = path.relative(repoRoot, filePath); + if (relative.startsWith('..')) return filePath; + return relative.split(path.sep).join('/'); +} + +function packageFor(filePath) { + const [firstSegment] = filePath.split('/'); + if (!firstSegment) return 'root'; + if (KNOWN_PACKAGES.has(firstSegment)) return firstSegment; + if (firstSegment === 'load-tests') return 'load-tests'; + if (firstSegment === 'scripts') return 'scripts'; + return firstSegment.startsWith('.') ? firstSegment : 'root'; +} + +function increment(map, key, amount = 1) { + map.set(key, (map.get(key) ?? 0) + amount); +} + +function table(headers, rows) { + const widths = headers.map((header, index) => + Math.max(header.length, ...rows.map((row) => String(row[index]).length)) + ); + const line = (cells) => + `| ${cells.map((cell, index) => String(cell).padEnd(widths[index], ' ')).join(' | ')} |`; + const separator = `| ${widths.map((width) => '-'.repeat(width)).join(' | ')} |`; + return [line(headers), separator, ...rows.map(line)].join('\n'); +} + +function summarize(results) { + const packages = new Map(); + const rules = new Map(); + const packageRules = new Map(); + const packageFiles = new Map(); + let errorCount = 0; + let warningCount = 0; + + for (const result of results) { + const filePath = relativeFile(result.filePath); + const packageName = packageFor(filePath); + + errorCount += result.errorCount; + warningCount += result.warningCount; + + if (result.warningCount > 0) { + increment(packages, packageName, result.warningCount); + increment(packageFiles, packageName); + } + + for (const message of result.messages) { + if (message.severity !== 1) continue; + const ruleId = message.ruleId ?? 'unknown'; + increment(rules, ruleId); + increment(packageRules, `${packageName}\t${ruleId}`); + } + } + + return { errorCount, warningCount, packages, rules, packageRules, packageFiles }; +} + +function sortedEntries(map) { + return [...map.entries()].sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0])); +} + +function printReport(summary, options) { + const budgetLabel = + options.maxWarnings === null + ? 'not enforced' + : `${options.maxWarnings} (${options.maxWarnings - summary.warningCount} remaining)`; + + console.log('Lint warning report'); + console.log(`ESLint target: ${options.eslintArgs.join(' ')}`); + console.log(`Errors: ${summary.errorCount}`); + console.log(`Warnings: ${summary.warningCount}`); + console.log(`Warning budget: ${budgetLabel}`); + + const packageRows = sortedEntries(summary.packages).map(([packageName, warnings]) => [ + packageName, + warnings, + summary.packageFiles.get(packageName) ?? 0, + ]); + if (packageRows.length > 0) { + console.log('\nWarnings by package'); + console.log(table(['Package', 'Warnings', 'Files'], packageRows)); + } + + const ruleRows = sortedEntries(summary.rules).map(([rule, warnings]) => [rule, warnings]); + if (ruleRows.length > 0) { + console.log('\nWarnings by rule'); + console.log(table(['Rule', 'Warnings'], ruleRows)); + } + + const packageRuleRows = sortedEntries(summary.packageRules) + .slice(0, options.showAllRules ? undefined : 25) + .map(([key, warnings]) => { + const [packageName, rule] = key.split('\t'); + return [packageName, rule, warnings]; + }); + if (packageRuleRows.length > 0) { + console.log('\nWarnings by package and rule'); + console.log(table(['Package', 'Rule', 'Warnings'], packageRuleRows)); + } +} + +let options; +try { + options = parseArgs(process.argv.slice(2)); +} catch (error) { + console.error(error instanceof Error ? error.message : String(error)); + process.exit(2); +} + +const eslint = spawnSync('eslint', [...options.eslintArgs, '--format', 'json'], { + cwd: repoRoot, + encoding: 'utf8', + maxBuffer: 1024 * 1024 * 64, + stdio: ['ignore', 'pipe', 'pipe'], +}); + +if (eslint.error) { + console.error(eslint.error.message); + process.exit(2); +} + +if (eslint.stderr.trim()) { + process.stderr.write(eslint.stderr); +} + +let results; +try { + results = JSON.parse(eslint.stdout); +} catch { + process.stdout.write(eslint.stdout); + console.error('Failed to parse ESLint JSON output.'); + process.exit(eslint.status || 2); +} + +const summary = summarize(results); +printReport(summary, options); + +if (summary.errorCount > 0) { + process.exitCode = 1; +} else if (options.maxWarnings !== null && summary.warningCount > options.maxWarnings) { + console.error( + `\nLint warning budget exceeded: ${summary.warningCount} warnings > ${options.maxWarnings}.` + ); + process.exitCode = 1; +} diff --git a/server/src/middleware/auth.ts b/server/src/middleware/auth.ts index 9aebf4b3..cf2a9cd4 100644 --- a/server/src/middleware/auth.ts +++ b/server/src/middleware/auth.ts @@ -3,7 +3,7 @@ import { WebSocket } from 'ws'; import { IncomingMessage } from 'http'; import crypto from 'crypto'; import jwt from 'jsonwebtoken'; -import { getSecurityConfig, getJwtSecret, getValidJwtSecrets } from '../config/security.js'; +import { getSecurityConfig, getValidJwtSecrets } from '../config/security.js'; import { createLogger } from '../lib/logger.js'; import { validateScopedApiToken } from '../services/api-token-service.js'; import { validateDeviceSessionSecret } from '../services/device-session-service.js'; @@ -389,14 +389,12 @@ function requestRemoteAddress(req: Request | IncomingMessage): string | null { */ function verifyJwtToken(token: string): { valid: boolean; error?: string } { const secrets = getValidJwtSecrets(); - let lastError: Error | null = null; for (const secret of secrets) { try { jwt.verify(token, secret, { algorithms: ['HS256'] }); return { valid: true }; } catch (err) { - lastError = err as Error; // If the token is expired, no point trying other secrets if (err instanceof jwt.TokenExpiredError) { return { valid: false, error: 'Session expired' }; diff --git a/server/src/middleware/validate.ts b/server/src/middleware/validate.ts index e738a236..9d640894 100644 --- a/server/src/middleware/validate.ts +++ b/server/src/middleware/validate.ts @@ -1,5 +1,5 @@ import { Request, Response, NextFunction } from 'express'; -import { z, ZodSchema, ZodError } from 'zod'; +import { ZodSchema, ZodError } from 'zod'; import { ValidationError } from './error-handler.js'; // Module augmentation: extend Express Request with validated data diff --git a/server/src/middleware/workflow-auth.ts b/server/src/middleware/workflow-auth.ts index c7ddc867..30920fdc 100644 --- a/server/src/middleware/workflow-auth.ts +++ b/server/src/middleware/workflow-auth.ts @@ -3,7 +3,7 @@ * Handles ACL checks for workflow operations */ -import type { WorkflowACL, WorkflowPermission } from '../types/workflow.js'; +import type { WorkflowPermission } from '../types/workflow.js'; import { getWorkflowService } from '../services/workflow-service.js'; import { ForbiddenError } from './error-handler.js'; diff --git a/server/src/routes/agent-status.ts b/server/src/routes/agent-status.ts index 8f8eca7d..1e0ce4d2 100644 --- a/server/src/routes/agent-status.ts +++ b/server/src/routes/agent-status.ts @@ -344,8 +344,6 @@ router.post( const squadChatEnabled = settings.enforcement?.squadChat ?? false; if (squadChatEnabled) { try { - // Import fireHook to post to squad chat - const { fireHook } = await import('../services/hook-service.js'); // Create a synthetic task for the squad chat message const violationMessage = `⚠️ Delegation Violation: ${agent} performed "${action}" directly instead of delegating to a sub-agent.${details ? ` Details: ${details}` : ''}${taskId ? ` (Task: ${taskId})` : ''}`; diff --git a/server/src/routes/agents.ts b/server/src/routes/agents.ts index be0dc8d3..e648b978 100644 --- a/server/src/routes/agents.ts +++ b/server/src/routes/agents.ts @@ -1,10 +1,6 @@ import { Router, type Router as RouterType } from 'express'; import { z } from 'zod'; -import { - AgentReadinessError, - ClawdbotAgentService, - clawdbotAgentService, -} from '../services/clawdbot-agent-service.js'; +import { AgentReadinessError, clawdbotAgentService } from '../services/clawdbot-agent-service.js'; import { getTelemetryService } from '../services/telemetry-service.js'; import { getTaskService } from '../services/task-service.js'; import type { AgentType, TokenTelemetryEvent } from '@veritas-kanban/shared'; diff --git a/server/src/routes/chat.ts b/server/src/routes/chat.ts index c67ff260..14c05950 100644 --- a/server/src/routes/chat.ts +++ b/server/src/routes/chat.ts @@ -14,7 +14,7 @@ import { ConfigService } from '../services/config-service.js'; import { getVeritasContextService } from '../services/veritas-context-service.js'; import type { ChatSendInput } from '@veritas-kanban/shared'; import { asyncHandler } from '../middleware/async-handler.js'; -import { NotFoundError, ValidationError } from '../middleware/error-handler.js'; +import { NotFoundError } from '../middleware/error-handler.js'; import { createLogger } from '../lib/logger.js'; const log = createLogger('chat'); diff --git a/server/src/routes/feedback.ts b/server/src/routes/feedback.ts index 357e6792..1387067d 100644 --- a/server/src/routes/feedback.ts +++ b/server/src/routes/feedback.ts @@ -1,14 +1,14 @@ import { Router, type Router as RouterType } from 'express'; import { z } from 'zod'; import { asyncHandler } from '../middleware/async-handler.js'; -import { BadRequestError, NotFoundError, ValidationError } from '../middleware/error-handler.js'; +import { NotFoundError, ValidationError } from '../middleware/error-handler.js'; import { feedbackService } from '../services/feedback-service.js'; import { paramStr, qNum, qStr } from '../lib/query-helpers.js'; const router: RouterType = Router(); const CATEGORIES = ['quality', 'performance', 'accuracy', 'safety', 'ux'] as const; -const SENTIMENTS = ['positive', 'neutral', 'negative'] as const; +type FeedbackSentiment = 'positive' | 'neutral' | 'negative'; const createFeedbackSchema = z.object({ taskId: z.string().min(1), @@ -43,14 +43,13 @@ router.get( asyncHandler(async (req, res) => { const limit = qNum(req.query.limit); const resolvedRaw = qStr(req.query.resolved); - const resolved = - resolvedRaw === 'true' ? true : resolvedRaw === 'false' ? false : undefined; + const resolved = resolvedRaw === 'true' ? true : resolvedRaw === 'false' ? false : undefined; const items = await feedbackService.list({ taskId: qStr(req.query.taskId), agent: qStr(req.query.agent), category: qStr(req.query.category) as (typeof CATEGORIES)[number] | undefined, - sentiment: qStr(req.query.sentiment) as (typeof SENTIMENTS)[number] | undefined, + sentiment: qStr(req.query.sentiment) as FeedbackSentiment | undefined, resolved, since: qStr(req.query.since), until: qStr(req.query.until), @@ -70,7 +69,7 @@ router.get( taskId: qStr(req.query.taskId), agent: qStr(req.query.agent), category: qStr(req.query.category) as (typeof CATEGORIES)[number] | undefined, - sentiment: qStr(req.query.sentiment) as (typeof SENTIMENTS)[number] | undefined, + sentiment: qStr(req.query.sentiment) as FeedbackSentiment | undefined, since: qStr(req.query.since), until: qStr(req.query.until), }); diff --git a/server/src/routes/integrations.ts b/server/src/routes/integrations.ts index a59d9a5f..73735aad 100644 --- a/server/src/routes/integrations.ts +++ b/server/src/routes/integrations.ts @@ -104,7 +104,7 @@ async function pingService(service: CoolifyServiceConfig): Promise controller.abort(), PING_TIMEOUT_MS); try { - const response = await fetch(validated.href, { + await fetch(validated.href, { method: 'HEAD', signal: controller.signal, redirect: 'manual', diff --git a/server/src/routes/metrics.ts b/server/src/routes/metrics.ts index 6a9414aa..e6fcca48 100644 --- a/server/src/routes/metrics.ts +++ b/server/src/routes/metrics.ts @@ -1,7 +1,6 @@ import { Router, type Router as RouterType } from 'express'; import { getMetricsService } from '../services/metrics/index.js'; import { asyncHandler } from '../middleware/async-handler.js'; -import { ValidationError } from '../middleware/error-handler.js'; import { validate, type ValidatedRequest } from '../middleware/validate.js'; import { MetricsQuerySchema, @@ -27,7 +26,7 @@ router.get( validate({ query: TaskMetricsQuerySchema }), asyncHandler(async (req: ValidatedRequest, res) => { const metrics = getMetricsService(); - const { project, period, from, to } = req.validated.query!; + const { project, period, from } = req.validated.query!; const { getPeriodStart } = await import('../services/metrics/helpers.js'); const since = getPeriodStart(period, from); const result = await metrics.getTaskMetrics(project, since); diff --git a/server/src/routes/projects.ts b/server/src/routes/projects.ts index 34c4facb..cf755722 100644 --- a/server/src/routes/projects.ts +++ b/server/src/routes/projects.ts @@ -1,4 +1,3 @@ -import { Router } from 'express'; import { z } from 'zod'; import { ProjectService } from '../services/project-service.js'; import { getTaskService } from '../services/task-service.js'; diff --git a/server/src/routes/sprints.ts b/server/src/routes/sprints.ts index b898c39e..9383626b 100644 --- a/server/src/routes/sprints.ts +++ b/server/src/routes/sprints.ts @@ -1,4 +1,3 @@ -import { Router } from 'express'; import { z } from 'zod'; import { SprintService } from '../services/sprint-service.js'; import { getTaskService } from '../services/task-service.js'; diff --git a/server/src/routes/status-history.ts b/server/src/routes/status-history.ts index 953c6628..6e9dcead 100644 --- a/server/src/routes/status-history.ts +++ b/server/src/routes/status-history.ts @@ -1,5 +1,4 @@ import { Router, type Router as RouterType } from 'express'; -import { z } from 'zod'; import { asyncHandler } from '../middleware/async-handler.js'; import { ValidationError } from '../middleware/error-handler.js'; import { authorize } from '../middleware/auth.js'; diff --git a/server/src/routes/task-archive.ts b/server/src/routes/task-archive.ts index fc42c6e6..e866f7c2 100644 --- a/server/src/routes/task-archive.ts +++ b/server/src/routes/task-archive.ts @@ -137,7 +137,7 @@ router.post( } else { failed.push(id); } - } catch (error) { + } catch { failed.push(id); } } diff --git a/server/src/routes/task-observations.ts b/server/src/routes/task-observations.ts index 389ed6b3..efc0eba9 100644 --- a/server/src/routes/task-observations.ts +++ b/server/src/routes/task-observations.ts @@ -6,7 +6,7 @@ import { activityService } from '../services/activity-service.js'; import { asyncHandler } from '../middleware/async-handler.js'; import { NotFoundError, ValidationError } from '../middleware/error-handler.js'; import { sanitizeCommentText } from '../utils/sanitize.js'; -import type { Task, Observation, ObservationType } from '@veritas-kanban/shared'; +import type { Observation, ObservationType } from '@veritas-kanban/shared'; import { qStr, qStrD, qNum, qNumD, paramStr } from '../lib/query-helpers.js'; const router: RouterType = Router(); diff --git a/server/src/routes/task-subtasks.ts b/server/src/routes/task-subtasks.ts index 3a34340a..37dc4ff0 100644 --- a/server/src/routes/task-subtasks.ts +++ b/server/src/routes/task-subtasks.ts @@ -22,10 +22,6 @@ const updateSubtaskSchema = z.object({ criteriaChecked: z.array(z.boolean()).optional(), }); -const toggleCriteriaSchema = z.object({ - criteriaIndex: z.number().int().min(0), -}); - // POST /api/tasks/:id/subtasks - Add subtask router.post( '/:id/subtasks', diff --git a/server/src/routes/task-types.ts b/server/src/routes/task-types.ts index a23382ae..1d9d4309 100644 --- a/server/src/routes/task-types.ts +++ b/server/src/routes/task-types.ts @@ -1,4 +1,3 @@ -import { Router } from 'express'; import { z } from 'zod'; import { TaskTypeService } from '../services/task-type-service.js'; import { getTaskService } from '../services/task-service.js'; diff --git a/server/src/services/analytics-service.ts b/server/src/services/analytics-service.ts index 9597ab85..a96c5bf6 100644 --- a/server/src/services/analytics-service.ts +++ b/server/src/services/analytics-service.ts @@ -1,5 +1,4 @@ -import { createLogger } from '../lib/logger.js'; -import type { Task, TimeEntry } from '@veritas-kanban/shared'; +import type { Task } from '@veritas-kanban/shared'; import { getTaskService } from './task-service.js'; import { StatusHistoryService } from './status-history-service.js'; import type { @@ -12,8 +11,6 @@ import type { MetricsQuery, } from '../schemas/analytics-schemas.js'; -const log = createLogger('analytics-service'); - /** * Time period with task info */ @@ -209,7 +206,7 @@ export class AnalyticsService { leadTimes.length > 0 ? leadTimes.reduce((a, b) => a + b, 0) / leadTimes.length : 0; // Calculate agent utilization - const agentPeriods = this.calculateAgentUtilization(tasksWithTime, from, to); + const agentPeriods = this.calculateAgentUtilization(tasksWithTime); // Total tracked time const totalTrackedTime = @@ -421,7 +418,7 @@ export class AnalyticsService { /** * Calculate agent utilization (working time per agent) */ - private calculateAgentUtilization(tasks: Task[], from: Date, to: Date): AgentPeriod[] { + private calculateAgentUtilization(tasks: Task[]): AgentPeriod[] { const agentMap = new Map< string, { startTime: number; endTime: number; totalDuration: number; taskCount: number } diff --git a/server/src/services/attachment-service.ts b/server/src/services/attachment-service.ts index ca5a70ca..5ee2fdf6 100644 --- a/server/src/services/attachment-service.ts +++ b/server/src/services/attachment-service.ts @@ -2,7 +2,6 @@ import fs from 'fs/promises'; import path from 'path'; import { createHash } from 'node:crypto'; import { nanoid } from 'nanoid'; -import mime from 'mime-types'; import type { Attachment, AttachmentLimits } from '@veritas-kanban/shared'; import { DEFAULT_ATTACHMENT_LIMITS, ALLOWED_MIME_TYPES } from '@veritas-kanban/shared'; import { validateMimeType, getAllowedTypesDescription } from './mime-validation.js'; diff --git a/server/src/services/broadcast-service.ts b/server/src/services/broadcast-service.ts index efba623c..72ef8069 100644 --- a/server/src/services/broadcast-service.ts +++ b/server/src/services/broadcast-service.ts @@ -1,4 +1,4 @@ -import type { WebSocketServer, WebSocket } from 'ws'; +import type { WebSocketServer } from 'ws'; import type { AnyTelemetryEvent, SquadMessage } from '@veritas-kanban/shared'; import type { AuthenticatedWebSocket } from '../middleware/auth.js'; import { diff --git a/server/src/services/changes-service.ts b/server/src/services/changes-service.ts index 98c4bc6e..eeb5157f 100644 --- a/server/src/services/changes-service.ts +++ b/server/src/services/changes-service.ts @@ -5,13 +5,7 @@ import { getTaskService } from './task-service.js'; import { activityService } from './activity-service.js'; -import type { - ChangesResponse, - ChangesQueryParams, - TaskChanges, - CommentChange, - BroadcastMessage, -} from '@veritas-kanban/shared'; +import type { ChangesResponse, ChangesQueryParams, CommentChange } from '@veritas-kanban/shared'; import { createLogger } from '../lib/logger.js'; const log = createLogger('changes-service'); diff --git a/server/src/services/chat-service.ts b/server/src/services/chat-service.ts index bb67e5bd..332b0ae3 100644 --- a/server/src/services/chat-service.ts +++ b/server/src/services/chat-service.ts @@ -22,8 +22,6 @@ const log = createLogger('chat-service'); // Default paths - resolve via shared paths helper to .veritas-kanban/chats/ const DEFAULT_CHATS_DIR = getChatsDir(); -const DEFAULT_SESSIONS_DIR = path.join(DEFAULT_CHATS_DIR, 'sessions'); -const DEFAULT_SQUAD_DIR = path.join(DEFAULT_CHATS_DIR, 'squad'); export interface ChatServiceOptions { chatsDir?: string; diff --git a/server/src/services/clawdbot-agent-service.ts b/server/src/services/clawdbot-agent-service.ts index e337c76a..81159bd9 100644 --- a/server/src/services/clawdbot-agent-service.ts +++ b/server/src/services/clawdbot-agent-service.ts @@ -47,7 +47,6 @@ const log = createLogger('clawdbot-agent-service'); const PROJECT_ROOT = path.resolve(process.cwd(), '..'); const LOGS_DIR = path.join(PROJECT_ROOT, '.veritas-kanban', 'logs'); -const CLAWDBOT_GATEWAY = process.env.CLAWDBOT_GATEWAY || 'http://127.0.0.1:18789'; export type AgentProvider = 'openclaw' | 'codex-cli' | 'codex-sdk'; const TRACE_SECRET_PATTERNS: Array<[RegExp, string]> = [ diff --git a/server/src/services/cost-prediction-service.ts b/server/src/services/cost-prediction-service.ts index 03834592..29a233af 100644 --- a/server/src/services/cost-prediction-service.ts +++ b/server/src/services/cost-prediction-service.ts @@ -182,7 +182,12 @@ class CostPredictionService { }; log.info( - { type: task.type, priority: task.priority, estimatedCost: prediction.estimatedCost, confidence }, + { + type: task.type, + priority: task.priority, + estimatedCost: prediction.estimatedCost, + confidence, + }, 'Cost prediction generated' ); @@ -256,7 +261,9 @@ class CostPredictionService { const meanAccuracy = accuracies.reduce((sum, a) => sum + a, 0) / accuracies.length; const medianAccuracy = sortedAccuracies.length % 2 === 0 - ? (sortedAccuracies[sortedAccuracies.length / 2 - 1]! + sortedAccuracies[sortedAccuracies.length / 2]!) / 2 + ? (sortedAccuracies[sortedAccuracies.length / 2 - 1]! + + sortedAccuracies[sortedAccuracies.length / 2]!) / + 2 : sortedAccuracies[Math.floor(sortedAccuracies.length / 2)]!; const within20 = accuracy.filter((a) => a.accuracy >= 80 && a.accuracy <= 120).length; @@ -274,8 +281,10 @@ class CostPredictionService { byType[type].meanError += item.error; } for (const type of Object.keys(byType)) { - byType[type].meanAccuracy = Math.round((byType[type].meanAccuracy / byType[type].count) * 10) / 10; - byType[type].meanError = Math.round((byType[type].meanError / byType[type].count) * 100) / 100; + byType[type].meanAccuracy = + Math.round((byType[type].meanAccuracy / byType[type].count) * 10) / 10; + byType[type].meanError = + Math.round((byType[type].meanError / byType[type].count) * 100) / 100; } return { @@ -293,8 +302,8 @@ class CostPredictionService { * Get historical average cost for similar tasks. */ private async getHistoricalBaseCost( - type?: string, - project?: string + _type?: string, + _project?: string ): Promise<{ avgCost: number; sampleSize: number }> { try { const telemetry = getTelemetryService(); @@ -316,8 +325,7 @@ class CostPredictionService { const inputTokens = (e.inputTokens as number) || 0; const outputTokens = (e.outputTokens as number) || 0; - const eventCost = - (e.cost as number) || inputTokens * 0.00001 + outputTokens * 0.00003; + const eventCost = (e.cost as number) || inputTokens * 0.00001 + outputTokens * 0.00003; taskCosts.set(taskId, (taskCosts.get(taskId) || 0) + eventCost); } diff --git a/server/src/services/diff-service.ts b/server/src/services/diff-service.ts index a9dc8bd8..deea8fdd 100644 --- a/server/src/services/diff-service.ts +++ b/server/src/services/diff-service.ts @@ -1,6 +1,5 @@ import { simpleGit } from 'simple-git'; import { TaskService } from './task-service.js'; -import type { Task } from '@veritas-kanban/shared'; import { createLogger } from '../lib/logger.js'; const log = createLogger('diff-service'); diff --git a/server/src/services/digest-service.ts b/server/src/services/digest-service.ts index 7bf778ac..a4aa3a4f 100644 --- a/server/src/services/digest-service.ts +++ b/server/src/services/digest-service.ts @@ -1,13 +1,7 @@ -import { - getMetricsService, - type MetricsService, - type TaskMetrics, - type RunMetrics, - type TokenMetrics, -} from './metrics/index.js'; +import { getMetricsService, type MetricsService } from './metrics/index.js'; import { getTelemetryService, type TelemetryService } from './telemetry-service.js'; import { TaskService } from './task-service.js'; -import type { Task, TaskTelemetryEvent } from '@veritas-kanban/shared'; +import type { TaskTelemetryEvent } from '@veritas-kanban/shared'; export interface DailyDigest { period: { diff --git a/server/src/services/error-learning-service.ts b/server/src/services/error-learning-service.ts index ae5581a9..8a78b07c 100644 --- a/server/src/services/error-learning-service.ts +++ b/server/src/services/error-learning-service.ts @@ -13,7 +13,6 @@ */ import { getTaskService } from './task-service.js'; -import { getTelemetryService } from './telemetry-service.js'; import { createLogger } from '../lib/logger.js'; import * as fs from 'node:fs/promises'; import * as path from 'node:path'; @@ -114,7 +113,12 @@ class ErrorLearningService { private async ensureLoaded(): Promise { if (!migrationChecked) { migrationChecked = true; - await migrateLegacyFiles(LEGACY_DATA_DIR, DATA_DIR, ['error-analyses.json'], 'error analysis'); + await migrateLegacyFiles( + LEGACY_DATA_DIR, + DATA_DIR, + ['error-analyses.json'], + 'error analysis' + ); } if (this.loaded) return; diff --git a/server/src/services/hook-service.ts b/server/src/services/hook-service.ts index 7d3e9d4b..88db746e 100644 --- a/server/src/services/hook-service.ts +++ b/server/src/services/hook-service.ts @@ -169,7 +169,7 @@ export async function fireHook( async function fireSquadChat( event: HookEvent, task: Pick, - previousStatus?: string + _previousStatus?: string ): Promise { const chatService = getChatService(); diff --git a/server/src/services/metrics/helpers.ts b/server/src/services/metrics/helpers.ts index dbe18b81..81e7b1c7 100644 --- a/server/src/services/metrics/helpers.ts +++ b/server/src/services/metrics/helpers.ts @@ -1,7 +1,6 @@ /** * Shared utility functions for metrics calculations. */ -import path from 'path'; import type { MetricsPeriod, TrendDirection } from './types.js'; import { getProjectRoot, getTelemetryDir } from '../../utils/paths.js'; diff --git a/server/src/services/metrics/metrics-service.ts b/server/src/services/metrics/metrics-service.ts index 07bf7ad4..31e08217 100644 --- a/server/src/services/metrics/metrics-service.ts +++ b/server/src/services/metrics/metrics-service.ts @@ -8,7 +8,12 @@ import { TELEMETRY_DIR } from './helpers.js'; import { computeTaskMetrics, computeVelocityMetrics } from './task-metrics.js'; import { computeRunMetrics, computeDurationMetrics, computeFailedRuns } from './run-metrics.js'; import { computeTokenMetrics, computeBudgetMetrics } from './token-metrics.js'; -import { computeAllMetrics, computeTrends, computeAgentComparison, computeUtilization } from './dashboard-metrics.js'; +import { + computeAllMetrics, + computeTrends, + computeAgentComparison, + computeUtilization, +} from './dashboard-metrics.js'; import type { MetricsPeriod, TaskMetrics, @@ -130,7 +135,7 @@ export class MetricsService { period: MetricsPeriod, from?: string, to?: string, - utcOffsetHours?: number, + utcOffsetHours?: number ): Promise { // Use telemetry-based computation (reliable data source) return computeUtilization(this.telemetryDir, period, from, to, utcOffsetHours); @@ -140,8 +145,8 @@ export class MetricsService { period: MetricsPeriod, project?: string, limit = 50, - from?: string, - to?: string + _from?: string, + _to?: string ): Promise { return computeFailedRuns(this.telemetryDir, period, project, limit); } diff --git a/server/src/services/metrics/task-metrics.ts b/server/src/services/metrics/task-metrics.ts index 397ad233..afd4d8a9 100644 --- a/server/src/services/metrics/task-metrics.ts +++ b/server/src/services/metrics/task-metrics.ts @@ -8,7 +8,6 @@ import { TaskService } from '../task-service.js'; import { PROJECT_ROOT } from './helpers.js'; import type { TaskMetrics, - MetricsPeriod, VelocityTrend, SprintVelocityPoint, CurrentSprintProgress, diff --git a/server/src/services/project-service.ts b/server/src/services/project-service.ts index 048b4ef2..4bf610b6 100644 --- a/server/src/services/project-service.ts +++ b/server/src/services/project-service.ts @@ -59,9 +59,6 @@ export class ProjectService extends ManagedListService { * Seed migration: scan all tasks and create ProjectConfig entries for unique projects */ private async seedProjectsFromTasks(): Promise { - const configDir = resolve(process.cwd(), '..', '.veritas-kanban'); - const projectsFile = resolve(configDir, 'projects.json'); - // Only seed if the file is empty or has no items const existingProjects = await this.list(true); if (existingProjects.length > 0) { diff --git a/server/src/services/prompt-registry-service.ts b/server/src/services/prompt-registry-service.ts index 3fd13c47..dfe9173e 100644 --- a/server/src/services/prompt-registry-service.ts +++ b/server/src/services/prompt-registry-service.ts @@ -11,7 +11,6 @@ import type { UpdatePromptTemplateInput, RenderPreviewRequest, RenderPreviewResponse, - PromptCategory, } from '@veritas-kanban/shared'; import { createLogger } from '../lib/logger.js'; import { validatePathSegment, ensureWithinBase } from '../utils/sanitize.js'; diff --git a/server/src/services/sprint-service.ts b/server/src/services/sprint-service.ts index ee15d9e7..0a910ffd 100644 --- a/server/src/services/sprint-service.ts +++ b/server/src/services/sprint-service.ts @@ -45,9 +45,6 @@ export class SprintService extends ManagedListService { * Seed migration: scan all tasks and create SprintConfig entries for unique sprints */ private async seedSprintsFromTasks(): Promise { - const configDir = resolve(process.cwd(), '..', '.veritas-kanban'); - const sprintsFile = resolve(configDir, 'sprints.json'); - // Only seed if the file is empty or has no items const existingSprints = await this.list(true); if (existingSprints.length > 0) { diff --git a/server/src/services/task-service.ts b/server/src/services/task-service.ts index e33c042a..f222cbf0 100644 --- a/server/src/services/task-service.ts +++ b/server/src/services/task-service.ts @@ -8,7 +8,6 @@ import type { CreateTaskInput, UpdateTaskInput, ReviewComment, - Subtask, TaskTelemetryEvent, TimeTracking, RunStartedEvent, diff --git a/server/src/services/text-extraction-service.ts b/server/src/services/text-extraction-service.ts index dc5f57e6..e5c534da 100644 --- a/server/src/services/text-extraction-service.ts +++ b/server/src/services/text-extraction-service.ts @@ -1,5 +1,4 @@ import fs from 'fs/promises'; -import path from 'path'; import { extractText as unpdfExtract } from 'unpdf'; import mammoth from 'mammoth'; import ExcelJS from 'exceljs'; @@ -133,13 +132,13 @@ export class TextExtractionService { // Extract all sheets const sheets: string[] = []; - workbook.eachSheet((worksheet, sheetId) => { + workbook.eachSheet((worksheet) => { const rows: string[] = []; - worksheet.eachRow((row, rowNumber) => { + worksheet.eachRow((row) => { const values: string[] = []; - row.eachCell({ includeEmpty: true }, (cell, colNumber) => { + row.eachCell({ includeEmpty: true }, (cell) => { // Get cell value as string const value = cell.value; diff --git a/server/src/services/worktree-service.ts b/server/src/services/worktree-service.ts index afeedaa0..57df0270 100644 --- a/server/src/services/worktree-service.ts +++ b/server/src/services/worktree-service.ts @@ -3,9 +3,7 @@ import path from 'path'; import { simpleGit, SimpleGit } from 'simple-git'; import { ConfigService } from './config-service.js'; import { TaskService } from './task-service.js'; -import type { Task } from '@veritas-kanban/shared'; import { spawn } from 'child_process'; -import { promisify } from 'util'; import { createLogger } from '../lib/logger.js'; const log = createLogger('worktree-service'); @@ -205,7 +203,7 @@ export class WorktreeService { // Get ahead/behind info let aheadBehind = { ahead: 0, behind: 0 }; try { - const { git: repoGit, repoPath: mainRepoPath } = await this.getRepoGit(task.git.repo); + const { repoPath: mainRepoPath } = await this.getRepoGit(task.git.repo); // Fetch to get latest with timeout (intentionally silent: may be offline) await this.execGitWithTimeout(mainRepoPath, ['fetch']).catch(() => {}); @@ -252,7 +250,7 @@ export class WorktreeService { } // Get main repo git - const { git: repoGit, repoPath } = await this.getRepoGit(task.git.repo); + const { repoPath } = await this.getRepoGit(task.git.repo); // Remove worktree with timeout const args = ['worktree', 'remove', worktreePath]; @@ -291,7 +289,7 @@ export class WorktreeService { throw new Error('Task does not have an active worktree'); } - const { git: repoGit, repoPath } = await this.getRepoGit(task.git.repo); + const { repoPath } = await this.getRepoGit(task.git.repo); // Checkout base branch in main repo with timeout await this.execGitWithTimeout(repoPath, ['checkout', task.git.baseBranch]); diff --git a/web/src/hooks/useToast.tsx b/web/src/hooks/useToast.tsx index add6fd79..ceb48591 100644 --- a/web/src/hooks/useToast.tsx +++ b/web/src/hooks/useToast.tsx @@ -20,13 +20,6 @@ type ToasterToast = ToastProps & { id: string; }; -const actionTypes = { - ADD_TOAST: 'ADD_TOAST', - UPDATE_TOAST: 'UPDATE_TOAST', - DISMISS_TOAST: 'DISMISS_TOAST', - REMOVE_TOAST: 'REMOVE_TOAST', -} as const; - let count = 0; function genId() { @@ -36,19 +29,19 @@ function genId() { type Action = | { - type: typeof actionTypes.ADD_TOAST; + type: 'ADD_TOAST'; toast: ToasterToast; } | { - type: typeof actionTypes.UPDATE_TOAST; + type: 'UPDATE_TOAST'; toast: Partial; } | { - type: typeof actionTypes.DISMISS_TOAST; + type: 'DISMISS_TOAST'; toastId?: ToasterToast['id']; } | { - type: typeof actionTypes.REMOVE_TOAST; + type: 'REMOVE_TOAST'; toastId?: ToasterToast['id']; }; diff --git a/web/src/lib/template-io.ts b/web/src/lib/template-io.ts index 08d5b1c1..430df4e9 100644 --- a/web/src/lib/template-io.ts +++ b/web/src/lib/template-io.ts @@ -97,7 +97,7 @@ export async function parseTemplateFile( let text: string; try { text = await file.text(); - } catch (err) { + } catch { throw new Error('Failed to read file'); } @@ -105,7 +105,7 @@ export async function parseTemplateFile( let parsed: unknown; try { parsed = JSON.parse(text); - } catch (err) { + } catch { throw new Error('Invalid JSON format'); }