fix: add complexity score to tm list and tm show (#1270)

This commit is contained in:
Ralph Khreish
2025-10-03 18:47:05 +02:00
committed by GitHub
parent f1393f47b1
commit 20004a39ea
14 changed files with 411 additions and 34 deletions

View File

@@ -33,6 +33,9 @@ export class TaskEntity implements Task {
tags?: string[];
assignee?: string;
complexity?: Task['complexity'];
recommendedSubtasks?: number;
expansionPrompt?: string;
complexityReasoning?: string;
constructor(data: Task | (Omit<Task, 'id'> & { id: number | string })) {
this.validate(data);
@@ -62,6 +65,9 @@ export class TaskEntity implements Task {
this.tags = data.tags;
this.assignee = data.assignee;
this.complexity = data.complexity;
this.recommendedSubtasks = data.recommendedSubtasks;
this.expansionPrompt = data.expansionPrompt;
this.complexityReasoning = data.complexityReasoning;
}
/**
@@ -246,7 +252,10 @@ export class TaskEntity implements Task {
actualEffort: this.actualEffort,
tags: this.tags,
assignee: this.assignee,
complexity: this.complexity
complexity: this.complexity,
recommendedSubtasks: this.recommendedSubtasks,
expansionPrompt: this.expansionPrompt,
complexityReasoning: this.complexityReasoning
};
}

View File

@@ -61,3 +61,12 @@ export { getLogger, createLogger, setGlobalLogger } from './logger/index.js';
// Re-export executors
export * from './executors/index.js';
// Re-export reports
export {
ComplexityReportManager,
type ComplexityReport,
type ComplexityReportMetadata,
type ComplexityAnalysis,
type TaskComplexityData
} from './reports/index.js';

View File

@@ -0,0 +1,185 @@
/**
* @fileoverview ComplexityReportManager - Handles loading and managing complexity analysis reports
* Follows the same pattern as ConfigManager and AuthManager
*/
import { promises as fs } from 'fs';
import path from 'path';
import type {
ComplexityReport,
ComplexityAnalysis,
TaskComplexityData
} from './types.js';
import { getLogger } from '../logger/index.js';
const logger = getLogger('ComplexityReportManager');
/**
* Manages complexity analysis reports
* Handles loading, caching, and providing complexity data for tasks
*/
export class ComplexityReportManager {
private projectRoot: string;
private reportCache: Map<string, ComplexityReport> = new Map();
constructor(projectRoot: string) {
this.projectRoot = projectRoot;
}
/**
* Get the path to the complexity report file for a given tag
*/
private getReportPath(tag?: string): string {
const reportsDir = path.join(this.projectRoot, '.taskmaster', 'reports');
const tagSuffix = tag && tag !== 'master' ? `_${tag}` : '';
return path.join(reportsDir, `task-complexity-report${tagSuffix}.json`);
}
/**
* Load complexity report for a given tag
* Results are cached to avoid repeated file reads
*/
async loadReport(tag?: string): Promise<ComplexityReport | null> {
const resolvedTag = tag || 'master';
const cacheKey = resolvedTag;
// Check cache first
if (this.reportCache.has(cacheKey)) {
return this.reportCache.get(cacheKey)!;
}
const reportPath = this.getReportPath(tag);
try {
// Check if file exists
await fs.access(reportPath);
// Read and parse the report
const content = await fs.readFile(reportPath, 'utf-8');
const report = JSON.parse(content) as ComplexityReport;
// Validate basic structure
if (!report.meta || !Array.isArray(report.complexityAnalysis)) {
logger.warn(
`Invalid complexity report structure at ${reportPath}, ignoring`
);
return null;
}
// Cache the report
this.reportCache.set(cacheKey, report);
logger.debug(
`Loaded complexity report for tag '${resolvedTag}' with ${report.complexityAnalysis.length} analyses`
);
return report;
} catch (error: any) {
if (error.code === 'ENOENT') {
// File doesn't exist - this is normal, not all projects have complexity reports
logger.debug(`No complexity report found for tag '${resolvedTag}'`);
return null;
}
// Other errors (parsing, permissions, etc.)
logger.warn(
`Failed to load complexity report for tag '${resolvedTag}': ${error.message}`
);
return null;
}
}
/**
* Get complexity data for a specific task ID
*/
async getComplexityForTask(
taskId: string | number,
tag?: string
): Promise<TaskComplexityData | null> {
const report = await this.loadReport(tag);
if (!report) {
return null;
}
// Find the analysis for this task
const analysis = report.complexityAnalysis.find(
(a) => String(a.taskId) === String(taskId)
);
if (!analysis) {
return null;
}
// Convert to TaskComplexityData format
return {
complexityScore: analysis.complexityScore,
recommendedSubtasks: analysis.recommendedSubtasks,
expansionPrompt: analysis.expansionPrompt,
complexityReasoning: analysis.complexityReasoning
};
}
/**
* Get complexity data for multiple tasks at once
* More efficient than calling getComplexityForTask multiple times
*/
async getComplexityForTasks(
taskIds: (string | number)[],
tag?: string
): Promise<Map<string, TaskComplexityData>> {
const result = new Map<string, TaskComplexityData>();
const report = await this.loadReport(tag);
if (!report) {
return result;
}
// Create a map for fast lookups
const analysisMap = new Map<string, ComplexityAnalysis>();
report.complexityAnalysis.forEach((analysis) => {
analysisMap.set(String(analysis.taskId), analysis);
});
// Map each task ID to its complexity data
taskIds.forEach((taskId) => {
const analysis = analysisMap.get(String(taskId));
if (analysis) {
result.set(String(taskId), {
complexityScore: analysis.complexityScore,
recommendedSubtasks: analysis.recommendedSubtasks,
expansionPrompt: analysis.expansionPrompt,
complexityReasoning: analysis.complexityReasoning
});
}
});
return result;
}
/**
* Clear the report cache
* @param tag - Specific tag to clear, or undefined to clear all cached reports
* Useful when reports are regenerated or modified externally
*/
clearCache(tag?: string): void {
if (tag) {
this.reportCache.delete(tag);
} else {
// Clear all cached reports
this.reportCache.clear();
}
}
/**
* Check if a complexity report exists for a tag
*/
async hasReport(tag?: string): Promise<boolean> {
const reportPath = this.getReportPath(tag);
try {
await fs.access(reportPath);
return true;
} catch {
return false;
}
}
}

View File

@@ -0,0 +1,11 @@
/**
* @fileoverview Reports module exports
*/
export { ComplexityReportManager } from './complexity-report-manager.js';
export type {
ComplexityReport,
ComplexityReportMetadata,
ComplexityAnalysis,
TaskComplexityData
} from './types.js';

View File

@@ -0,0 +1,65 @@
/**
* @fileoverview Type definitions for complexity analysis reports
*/
/**
* Analysis result for a single task
*/
export interface ComplexityAnalysis {
/** Task ID being analyzed */
taskId: string | number;
/** Task title */
taskTitle: string;
/** Complexity score (1-10 scale) */
complexityScore: number;
/** Recommended number of subtasks */
recommendedSubtasks: number;
/** AI-generated prompt for task expansion */
expansionPrompt: string;
/** Reasoning behind the complexity assessment */
complexityReasoning: string;
}
/**
* Metadata about the complexity report
*/
export interface ComplexityReportMetadata {
/** When the report was generated */
generatedAt: string;
/** Number of tasks analyzed in this run */
tasksAnalyzed: number;
/** Total number of tasks in the file */
totalTasks?: number;
/** Total analyses in the report (across all runs) */
analysisCount?: number;
/** Complexity threshold score used */
thresholdScore: number;
/** Project name */
projectName?: string;
/** Whether research mode was used */
usedResearch: boolean;
}
/**
* Complete complexity analysis report
*/
export interface ComplexityReport {
/** Report metadata */
meta: ComplexityReportMetadata;
/** Array of complexity analyses */
complexityAnalysis: ComplexityAnalysis[];
}
/**
* Complexity data to be attached to a Task
*/
export interface TaskComplexityData {
/** Complexity score (1-10 scale) */
complexityScore?: number;
/** Recommended number of subtasks */
recommendedSubtasks?: number;
/** AI-generated expansion prompt */
expansionPrompt?: string;
/** Reasoning behind the assessment */
complexityReasoning?: string;
}

View File

@@ -397,16 +397,6 @@ export class TaskService {
}
}
// Complexity filter
if (filter.complexity) {
const complexities = Array.isArray(filter.complexity)
? filter.complexity
: [filter.complexity];
if (!task.complexity || !complexities.includes(task.complexity)) {
return false;
}
}
// Search filter
if (filter.search) {
const searchLower = filter.search.toLowerCase();

View File

@@ -11,6 +11,7 @@ import type {
import { FormatHandler } from './format-handler.js';
import { FileOperations } from './file-operations.js';
import { PathResolver } from './path-resolver.js';
import { ComplexityReportManager } from '../../reports/complexity-report-manager.js';
/**
* File-based storage implementation using a single tasks.json file with separated concerns
@@ -19,11 +20,13 @@ export class FileStorage implements IStorage {
private formatHandler: FormatHandler;
private fileOps: FileOperations;
private pathResolver: PathResolver;
private complexityManager: ComplexityReportManager;
constructor(projectPath: string) {
this.formatHandler = new FormatHandler();
this.fileOps = new FileOperations();
this.pathResolver = new PathResolver(projectPath);
this.complexityManager = new ComplexityReportManager(projectPath);
}
/**
@@ -87,6 +90,7 @@ export class FileStorage implements IStorage {
/**
* Load tasks from the single tasks.json file for a specific tag
* Enriches tasks with complexity data from the complexity report
*/
async loadTasks(tag?: string): Promise<Task[]> {
const filePath = this.pathResolver.getTasksPath();
@@ -94,7 +98,10 @@ export class FileStorage implements IStorage {
try {
const rawData = await this.fileOps.readJson(filePath);
return this.formatHandler.extractTasks(rawData, resolvedTag);
const tasks = this.formatHandler.extractTasks(rawData, resolvedTag);
// Enrich tasks with complexity data
return await this.enrichTasksWithComplexity(tasks, resolvedTag);
} catch (error: any) {
if (error.code === 'ENOENT') {
return []; // File doesn't exist, return empty array
@@ -596,6 +603,46 @@ export class FileStorage implements IStorage {
await this.saveTasks(tasks, targetTag);
}
/**
* Enrich tasks with complexity data from the complexity report
* Private helper method called by loadTasks()
*/
private async enrichTasksWithComplexity(
tasks: Task[],
tag: string
): Promise<Task[]> {
// Get all task IDs for bulk lookup
const taskIds = tasks.map((t) => t.id);
// Load complexity data for all tasks at once (more efficient)
const complexityMap = await this.complexityManager.getComplexityForTasks(
taskIds,
tag
);
// If no complexity data found, return tasks as-is
if (complexityMap.size === 0) {
return tasks;
}
// Enrich each task with its complexity data
return tasks.map((task) => {
const complexityData = complexityMap.get(String(task.id));
if (!complexityData) {
return task;
}
// Merge complexity data into the task
return {
...task,
complexity: complexityData.complexityScore,
recommendedSubtasks: complexityData.recommendedSubtasks,
expansionPrompt: complexityData.expansionPrompt,
complexityReasoning: complexityData.complexityReasoning
};
});
}
}
// Export as default for convenience

View File

@@ -72,7 +72,13 @@ export interface Task {
actualEffort?: number;
tags?: string[];
assignee?: string;
complexity?: TaskComplexity;
// Complexity analysis (from complexity report)
// Can be either enum ('simple' | 'moderate' | 'complex' | 'very-complex') or numeric score (1-10)
complexity?: TaskComplexity | number;
recommendedSubtasks?: number;
expansionPrompt?: string;
complexityReasoning?: string;
}
/**
@@ -145,7 +151,6 @@ export interface TaskFilter {
hasSubtasks?: boolean;
search?: string;
assignee?: string;
complexity?: TaskComplexity | TaskComplexity[];
}
/**