fix: add complexity score to tm list and tm show (#1270)
This commit is contained in:
@@ -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
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -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';
|
||||
|
||||
185
packages/tm-core/src/reports/complexity-report-manager.ts
Normal file
185
packages/tm-core/src/reports/complexity-report-manager.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
11
packages/tm-core/src/reports/index.ts
Normal file
11
packages/tm-core/src/reports/index.ts
Normal 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';
|
||||
65
packages/tm-core/src/reports/types.ts
Normal file
65
packages/tm-core/src/reports/types.ts
Normal 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;
|
||||
}
|
||||
@@ -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();
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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[];
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user