Ratcheting lint warning budget

This commit is contained in:
Brad Groux 2026-06-04 00:16:42 -07:00
parent e360ef3f16
commit a42988a3e2
45 changed files with 331 additions and 112 deletions

View file

@ -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

View file

@ -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);

View file

@ -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(

View file

@ -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.

View file

@ -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.

View file

@ -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",

View file

@ -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;
}

View file

@ -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' };

View file

@ -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

View file

@ -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';

View file

@ -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})` : ''}`;

View file

@ -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';

View file

@ -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');

View file

@ -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),
});

View file

@ -104,7 +104,7 @@ async function pingService(service: CoolifyServiceConfig): Promise<ServiceStatus
const timeout = setTimeout(() => controller.abort(), PING_TIMEOUT_MS);
try {
const response = await fetch(validated.href, {
await fetch(validated.href, {
method: 'HEAD',
signal: controller.signal,
redirect: 'manual',

View file

@ -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<unknown, TaskMetricsQuery>, 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);

View file

@ -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';

View file

@ -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';

View file

@ -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';

View file

@ -137,7 +137,7 @@ router.post(
} else {
failed.push(id);
}
} catch (error) {
} catch {
failed.push(id);
}
}

View file

@ -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();

View file

@ -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',

View file

@ -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';

View file

@ -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 }

View file

@ -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';

View file

@ -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 {

View file

@ -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');

View file

@ -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;

View file

@ -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]> = [

View file

@ -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);
}

View file

@ -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');

View file

@ -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: {

View file

@ -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<void> {
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;

View file

@ -169,7 +169,7 @@ export async function fireHook(
async function fireSquadChat(
event: HookEvent,
task: Pick<Task, 'id' | 'title' | 'status' | 'project' | 'sprint' | 'agent'>,
previousStatus?: string
_previousStatus?: string
): Promise<void> {
const chatService = getChatService();

View file

@ -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';

View file

@ -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<import('./types.js').UtilizationMetrics> {
// 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<FailedRunDetails[]> {
return computeFailedRuns(this.telemetryDir, period, project, limit);
}

View file

@ -8,7 +8,6 @@ import { TaskService } from '../task-service.js';
import { PROJECT_ROOT } from './helpers.js';
import type {
TaskMetrics,
MetricsPeriod,
VelocityTrend,
SprintVelocityPoint,
CurrentSprintProgress,

View file

@ -59,9 +59,6 @@ export class ProjectService extends ManagedListService<ProjectConfig> {
* Seed migration: scan all tasks and create ProjectConfig entries for unique projects
*/
private async seedProjectsFromTasks(): Promise<void> {
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) {

View file

@ -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';

View file

@ -45,9 +45,6 @@ export class SprintService extends ManagedListService<SprintConfig> {
* Seed migration: scan all tasks and create SprintConfig entries for unique sprints
*/
private async seedSprintsFromTasks(): Promise<void> {
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) {

View file

@ -8,7 +8,6 @@ import type {
CreateTaskInput,
UpdateTaskInput,
ReviewComment,
Subtask,
TaskTelemetryEvent,
TimeTracking,
RunStartedEvent,

View file

@ -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;

View file

@ -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]);

View file

@ -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<ToasterToast>;
}
| {
type: typeof actionTypes.DISMISS_TOAST;
type: 'DISMISS_TOAST';
toastId?: ToasterToast['id'];
}
| {
type: typeof actionTypes.REMOVE_TOAST;
type: 'REMOVE_TOAST';
toastId?: ToasterToast['id'];
};

View file

@ -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');
}