mirror of
https://github.com/BradGroux/veritas-kanban.git
synced 2026-08-28 02:44:59 +00:00
Ratcheting lint warning budget
This commit is contained in:
parent
e360ef3f16
commit
a42988a3e2
45 changed files with 331 additions and 112 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
69
docs/testing/lint-warning-debt.md
Normal file
69
docs/testing/lint-warning-debt.md
Normal 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.
|
||||
|
|
@ -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",
|
||||
|
|
|
|||
192
scripts/lint-warning-budget.mjs
Normal file
192
scripts/lint-warning-budget.mjs
Normal 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;
|
||||
}
|
||||
|
|
@ -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' };
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
||||
|
|
|
|||
|
|
@ -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})` : ''}`;
|
||||
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
|
|
|||
|
|
@ -137,7 +137,7 @@ router.post(
|
|||
} else {
|
||||
failed.push(id);
|
||||
}
|
||||
} catch (error) {
|
||||
} catch {
|
||||
failed.push(id);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
|
|
|||
|
|
@ -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 }
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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]> = [
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
|
||||
|
|
|
|||
|
|
@ -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: {
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,7 +8,6 @@ import { TaskService } from '../task-service.js';
|
|||
import { PROJECT_ROOT } from './helpers.js';
|
||||
import type {
|
||||
TaskMetrics,
|
||||
MetricsPeriod,
|
||||
VelocityTrend,
|
||||
SprintVelocityPoint,
|
||||
CurrentSprintProgress,
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -8,7 +8,6 @@ import type {
|
|||
CreateTaskInput,
|
||||
UpdateTaskInput,
|
||||
ReviewComment,
|
||||
Subtask,
|
||||
TaskTelemetryEvent,
|
||||
TimeTracking,
|
||||
RunStartedEvent,
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
||||
|
|
|
|||
|
|
@ -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]);
|
||||
|
|
|
|||
|
|
@ -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'];
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue