refactor(tm-core): migrate to Vitest and Biome, implement clean architecture

- Migrated from Jest to Vitest for faster test execution (~4.2s vs ~4.6-5s)
- Replaced ESLint and Prettier with Biome for unified, faster linting/formatting
- Implemented BaseProvider with Template Method pattern following clean code principles
- Created TaskEntity with business logic encapsulation
- Added TaskMasterCore facade as main entry point
- Implemented complete end-to-end listTasks functionality
- All 50 tests passing with improved performance

🤖 Generated with Claude Code

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Ralph Khreish
2025-08-20 23:32:09 +02:00
parent aee1996dc2
commit cf6533207f
31 changed files with 3247 additions and 4310 deletions

View File

@@ -3,7 +3,6 @@
*/
import { z } from 'zod';
import { TaskPriority, TaskComplexity } from '../types/index.js';
// ============================================================================
// Enum Schemas
@@ -17,12 +16,7 @@ export const taskPrioritySchema = z.enum(['low', 'medium', 'high', 'critical']);
/**
* Task complexity validation schema
*/
export const taskComplexitySchema = z.enum([
'simple',
'moderate',
'complex',
'very-complex'
]);
export const taskComplexitySchema = z.enum(['simple', 'moderate', 'complex', 'very-complex']);
/**
* Log level validation schema
@@ -37,11 +31,7 @@ export const storageTypeSchema = z.enum(['file', 'memory', 'database']);
/**
* Tag naming convention validation schema
*/
export const tagNamingConventionSchema = z.enum([
'kebab-case',
'camelCase',
'snake_case'
]);
export const tagNamingConventionSchema = z.enum(['kebab-case', 'camelCase', 'snake_case']);
/**
* Buffer encoding validation schema
@@ -233,6 +223,4 @@ export const cacheConfigSchema = z
// ============================================================================
export type ConfigurationSchema = z.infer<typeof configurationSchema>;
export type PartialConfigurationSchema = z.infer<
typeof partialConfigurationSchema
>;
export type PartialConfigurationSchema = z.infer<typeof partialConfigurationSchema>;

View File

@@ -0,0 +1,239 @@
/**
* @fileoverview Task entity with business rules and domain logic
*/
import { ERROR_CODES, TaskMasterError } from '../../errors/task-master-error.js';
import type { Subtask, Task, TaskPriority, TaskStatus } from '../../types/index.js';
/**
* Task entity representing a task with business logic
* Encapsulates validation and state management rules
*/
export class TaskEntity implements Task {
readonly id: string;
title: string;
description: string;
status: TaskStatus;
priority: TaskPriority;
dependencies: string[];
details: string;
testStrategy: string;
subtasks: Subtask[];
// Optional properties
createdAt?: string;
updatedAt?: string;
effort?: number;
actualEffort?: number;
tags?: string[];
assignee?: string;
complexity?: Task['complexity'];
constructor(data: Task) {
this.validate(data);
this.id = data.id;
this.title = data.title;
this.description = data.description;
this.status = data.status;
this.priority = data.priority;
this.dependencies = data.dependencies || [];
this.details = data.details;
this.testStrategy = data.testStrategy;
this.subtasks = data.subtasks || [];
// Optional properties
this.createdAt = data.createdAt;
this.updatedAt = data.updatedAt;
this.effort = data.effort;
this.actualEffort = data.actualEffort;
this.tags = data.tags;
this.assignee = data.assignee;
this.complexity = data.complexity;
}
/**
* Validate task data
*/
private validate(data: Partial<Task>): void {
if (!data.id || typeof data.id !== 'string') {
throw new TaskMasterError(
'Task ID is required and must be a string',
ERROR_CODES.VALIDATION_ERROR
);
}
if (!data.title || data.title.trim().length === 0) {
throw new TaskMasterError('Task title is required', ERROR_CODES.VALIDATION_ERROR);
}
if (!data.description || data.description.trim().length === 0) {
throw new TaskMasterError('Task description is required', ERROR_CODES.VALIDATION_ERROR);
}
if (!this.isValidStatus(data.status)) {
throw new TaskMasterError(
`Invalid task status: ${data.status}`,
ERROR_CODES.VALIDATION_ERROR
);
}
if (!this.isValidPriority(data.priority)) {
throw new TaskMasterError(
`Invalid task priority: ${data.priority}`,
ERROR_CODES.VALIDATION_ERROR
);
}
}
/**
* Check if status is valid
*/
private isValidStatus(status: any): status is TaskStatus {
return [
'pending',
'in-progress',
'done',
'deferred',
'cancelled',
'blocked',
'review'
].includes(status);
}
/**
* Check if priority is valid
*/
private isValidPriority(priority: any): priority is TaskPriority {
return ['low', 'medium', 'high', 'critical'].includes(priority);
}
/**
* Check if task can be marked as complete
*/
canComplete(): boolean {
// Cannot complete if status is already done or cancelled
if (this.status === 'done' || this.status === 'cancelled') {
return false;
}
// Cannot complete if blocked
if (this.status === 'blocked') {
return false;
}
// Check if all subtasks are complete
const allSubtasksComplete = this.subtasks.every(
(subtask) => subtask.status === 'done' || subtask.status === 'cancelled'
);
return allSubtasksComplete;
}
/**
* Mark task as complete
*/
markAsComplete(): void {
if (!this.canComplete()) {
throw new TaskMasterError(
'Task cannot be marked as complete',
ERROR_CODES.TASK_STATUS_ERROR,
{
taskId: this.id,
currentStatus: this.status,
hasIncompleteSubtasks: this.subtasks.some(
(s) => s.status !== 'done' && s.status !== 'cancelled'
)
}
);
}
this.status = 'done';
this.updatedAt = new Date().toISOString();
}
/**
* Check if task has dependencies
*/
hasDependencies(): boolean {
return this.dependencies.length > 0;
}
/**
* Check if task has subtasks
*/
hasSubtasks(): boolean {
return this.subtasks.length > 0;
}
/**
* Add a subtask
*/
addSubtask(subtask: Omit<Subtask, 'id' | 'parentId'>): void {
const nextId = this.subtasks.length + 1;
this.subtasks.push({
...subtask,
id: nextId,
parentId: this.id
});
this.updatedAt = new Date().toISOString();
}
/**
* Update task status
*/
updateStatus(newStatus: TaskStatus): void {
if (!this.isValidStatus(newStatus)) {
throw new TaskMasterError(`Invalid status: ${newStatus}`, ERROR_CODES.VALIDATION_ERROR);
}
// Business rule: Cannot move from done to pending
if (this.status === 'done' && newStatus === 'pending') {
throw new TaskMasterError(
'Cannot move completed task back to pending',
ERROR_CODES.TASK_STATUS_ERROR
);
}
this.status = newStatus;
this.updatedAt = new Date().toISOString();
}
/**
* Convert entity to plain object
*/
toJSON(): Task {
return {
id: this.id,
title: this.title,
description: this.description,
status: this.status,
priority: this.priority,
dependencies: this.dependencies,
details: this.details,
testStrategy: this.testStrategy,
subtasks: this.subtasks,
createdAt: this.createdAt,
updatedAt: this.updatedAt,
effort: this.effort,
actualEffort: this.actualEffort,
tags: this.tags,
assignee: this.assignee,
complexity: this.complexity
};
}
/**
* Create TaskEntity from plain object
*/
static fromObject(data: Task): TaskEntity {
return new TaskEntity(data);
}
/**
* Create multiple TaskEntities from array
*/
static fromArray(data: Task[]): TaskEntity[] {
return data.map((task) => new TaskEntity(task));
}
}

View File

@@ -174,8 +174,8 @@ export class TaskMasterError extends Error {
}
// If we have a cause error, append its stack trace
if (cause && cause.stack) {
this.stack = this.stack + '\nCaused by: ' + cause.stack;
if (cause?.stack) {
this.stack = `${this.stack}\nCaused by: ${cause.stack}`;
}
}
@@ -211,14 +211,7 @@ export class TaskMasterError extends Error {
private containsSensitiveInfo(obj: any): boolean {
if (typeof obj !== 'object' || obj === null) return false;
const sensitiveKeys = [
'password',
'token',
'key',
'secret',
'auth',
'credential'
];
const sensitiveKeys = ['password', 'token', 'key', 'secret', 'auth', 'credential'];
const objString = JSON.stringify(obj).toLowerCase();
return sensitiveKeys.some((key) => objString.includes(key));
@@ -300,9 +293,7 @@ export class TaskMasterError extends Error {
/**
* Create a new error with additional context
*/
public withContext(
additionalContext: Partial<ErrorContext>
): TaskMasterError {
public withContext(additionalContext: Partial<ErrorContext>): TaskMasterError {
return new TaskMasterError(
this.message,
this.code,

View File

@@ -3,6 +3,14 @@
* This file exports all public APIs from the core Task Master library
*/
// Export main facade
export {
TaskMasterCore,
createTaskMasterCore,
type TaskMasterCoreOptions,
type ListTasksResult
} from './task-master-core.js';
// Re-export types
export type * from './types/index';
@@ -25,6 +33,9 @@ export * from './utils/index';
// Re-export errors
export * from './errors/index';
// Re-export entities
export { TaskEntity } from './core/entities/task.entity.js';
// Package metadata
export const version = '1.0.0';
export const name = '@task-master/tm-core';

View File

@@ -282,10 +282,7 @@ export abstract class BaseAIProvider implements IAIProvider {
}
// Abstract methods that must be implemented by concrete classes
abstract generateCompletion(
prompt: string,
options?: AIOptions
): Promise<AIResponse>;
abstract generateCompletion(prompt: string, options?: AIOptions): Promise<AIResponse>;
abstract generateStreamingCompletion(
prompt: string,
options?: AIOptions
@@ -309,9 +306,7 @@ export abstract class BaseAIProvider implements IAIProvider {
const modelExists = availableModels.some((m) => m.id === model);
if (!modelExists) {
throw new Error(
`Model "${model}" is not available for provider "${this.getName()}"`
);
throw new Error(`Model "${model}" is not available for provider "${this.getName()}"`);
}
this.currentModel = model;
@@ -347,11 +342,7 @@ export abstract class BaseAIProvider implements IAIProvider {
* @param duration - Request duration in milliseconds
* @param success - Whether the request was successful
*/
protected updateUsageStats(
response: AIResponse,
duration: number,
success: boolean
): void {
protected updateUsageStats(response: AIResponse, duration: number, success: boolean): void {
if (!this.usageStats) return;
this.usageStats.totalRequests++;
@@ -370,18 +361,15 @@ export abstract class BaseAIProvider implements IAIProvider {
}
// Update average response time
const totalTime =
this.usageStats.averageResponseTime * (this.usageStats.totalRequests - 1);
this.usageStats.averageResponseTime =
(totalTime + duration) / this.usageStats.totalRequests;
const totalTime = this.usageStats.averageResponseTime * (this.usageStats.totalRequests - 1);
this.usageStats.averageResponseTime = (totalTime + duration) / this.usageStats.totalRequests;
// Update success rate
const successCount = Math.floor(
this.usageStats.successRate * (this.usageStats.totalRequests - 1)
);
const newSuccessCount = successCount + (success ? 1 : 0);
this.usageStats.successRate =
newSuccessCount / this.usageStats.totalRequests;
this.usageStats.successRate = newSuccessCount / this.usageStats.totalRequests;
this.usageStats.lastRequestAt = new Date().toISOString();
}

View File

@@ -3,7 +3,7 @@
* This file defines the contract for configuration management
*/
import type { TaskPriority, TaskComplexity } from '../types/index';
import type { TaskComplexity, TaskPriority } from '../types/index';
/**
* Model configuration for different AI roles

View File

@@ -40,11 +40,7 @@ export interface IStorage {
* @param tag - Optional tag context for the task
* @returns Promise that resolves when update is complete
*/
updateTask(
taskId: string,
updates: Partial<Task>,
tag?: string
): Promise<void>;
updateTask(taskId: string, updates: Partial<Task>, tag?: string): Promise<void>;
/**
* Delete a task by ID
@@ -177,11 +173,7 @@ export abstract class BaseStorage implements IStorage {
abstract loadTasks(tag?: string): Promise<Task[]>;
abstract saveTasks(tasks: Task[], tag?: string): Promise<void>;
abstract appendTasks(tasks: Task[], tag?: string): Promise<void>;
abstract updateTask(
taskId: string,
updates: Partial<Task>,
tag?: string
): Promise<void>;
abstract updateTask(taskId: string, updates: Partial<Task>, tag?: string): Promise<void>;
abstract deleteTask(taskId: string, tag?: string): Promise<void>;
abstract exists(tag?: string): Promise<boolean>;
abstract loadMetadata(tag?: string): Promise<TaskMetadata | null>;

View File

@@ -22,9 +22,7 @@ export interface TaskParser {
export class PlaceholderParser implements TaskParser {
async parse(content: string): Promise<PlaceholderTask[]> {
// Simple placeholder parsing logic
const lines = content
.split('\n')
.filter((line) => line.trim().startsWith('-'));
const lines = content.split('\n').filter((line) => line.trim().startsWith('-'));
return lines.map((line, index) => ({
id: `task-${index + 1}`,
title: line.trim().replace(/^-\s*/, ''),

View File

@@ -0,0 +1,405 @@
/**
* @fileoverview Abstract base provider with Template Method pattern for AI providers
* Provides common functionality, error handling, and retry logic
*/
import { ERROR_CODES, TaskMasterError } from '../../errors/task-master-error.js';
import type { AIOptions, AIResponse, IAIProvider } from '../../interfaces/ai-provider.interface.js';
// Constants for retry logic
const DEFAULT_MAX_RETRIES = 3;
const BASE_RETRY_DELAY_MS = 1000;
const MAX_RETRY_DELAY_MS = 32000;
const BACKOFF_MULTIPLIER = 2;
const JITTER_FACTOR = 0.1;
// Constants for validation
const MIN_PROMPT_LENGTH = 1;
const MAX_PROMPT_LENGTH = 100000;
const MIN_TEMPERATURE = 0;
const MAX_TEMPERATURE = 2;
const MIN_MAX_TOKENS = 1;
const MAX_MAX_TOKENS = 100000;
/**
* Configuration for BaseProvider
*/
export interface BaseProviderConfig {
apiKey: string;
model?: string;
}
/**
* Internal completion result structure
*/
export interface CompletionResult {
content: string;
inputTokens?: number;
outputTokens?: number;
finishReason?: string;
model?: string;
}
/**
* Validation result for input validation
*/
interface ValidationResult {
valid: boolean;
error?: string;
}
/**
* Prepared request after preprocessing
*/
interface PreparedRequest {
prompt: string;
options: AIOptions;
metadata: Record<string, any>;
}
/**
* Abstract base provider implementing Template Method pattern
* Provides common error handling, retry logic, and validation
*/
export abstract class BaseProvider implements IAIProvider {
protected readonly apiKey: string;
protected model: string;
constructor(config: BaseProviderConfig) {
if (!config.apiKey) {
throw new TaskMasterError('API key is required', ERROR_CODES.AUTHENTICATION_ERROR);
}
this.apiKey = config.apiKey;
this.model = config.model || this.getDefaultModel();
}
/**
* Template method for generating completions
* Handles validation, retries, and error handling
*/
async generateCompletion(prompt: string, options?: AIOptions): Promise<AIResponse> {
// Validate input
const validation = this.validateInput(prompt, options);
if (!validation.valid) {
throw new TaskMasterError(validation.error || 'Invalid input', ERROR_CODES.VALIDATION_ERROR);
}
// Prepare request
const prepared = this.prepareRequest(prompt, options);
// Execute with retry logic
let lastError: Error | undefined;
const maxRetries = this.getMaxRetries();
for (let attempt = 1; attempt <= maxRetries; attempt++) {
try {
const startTime = Date.now();
const result = await this.generateCompletionInternal(prepared.prompt, prepared.options);
const duration = Date.now() - startTime;
return this.handleResponse(result, duration, prepared);
} catch (error) {
lastError = error as Error;
if (!this.shouldRetry(error, attempt)) {
break;
}
const delay = this.calculateBackoffDelay(attempt);
await this.sleep(delay);
}
}
// All retries failed
this.handleError(lastError || new Error('Unknown error'));
}
/**
* Validate input prompt and options
*/
protected validateInput(prompt: string, options?: AIOptions): ValidationResult {
// Validate prompt
if (!prompt || typeof prompt !== 'string') {
return { valid: false, error: 'Prompt must be a non-empty string' };
}
const trimmedPrompt = prompt.trim();
if (trimmedPrompt.length < MIN_PROMPT_LENGTH) {
return { valid: false, error: 'Prompt cannot be empty' };
}
if (trimmedPrompt.length > MAX_PROMPT_LENGTH) {
return {
valid: false,
error: `Prompt exceeds maximum length of ${MAX_PROMPT_LENGTH} characters`
};
}
// Validate options if provided
if (options) {
const optionValidation = this.validateOptions(options);
if (!optionValidation.valid) {
return optionValidation;
}
}
return { valid: true };
}
/**
* Validate completion options
*/
protected validateOptions(options: AIOptions): ValidationResult {
if (options.temperature !== undefined) {
if (options.temperature < MIN_TEMPERATURE || options.temperature > MAX_TEMPERATURE) {
return {
valid: false,
error: `Temperature must be between ${MIN_TEMPERATURE} and ${MAX_TEMPERATURE}`
};
}
}
if (options.maxTokens !== undefined) {
if (options.maxTokens < MIN_MAX_TOKENS || options.maxTokens > MAX_MAX_TOKENS) {
return {
valid: false,
error: `Max tokens must be between ${MIN_MAX_TOKENS} and ${MAX_MAX_TOKENS}`
};
}
}
if (options.topP !== undefined) {
if (options.topP < 0 || options.topP > 1) {
return { valid: false, error: 'Top-p must be between 0 and 1' };
}
}
return { valid: true };
}
/**
* Prepare request for processing
*/
protected prepareRequest(prompt: string, options?: AIOptions): PreparedRequest {
const defaultOptions = this.getDefaultOptions();
const mergedOptions = { ...defaultOptions, ...options };
return {
prompt: prompt.trim(),
options: mergedOptions,
metadata: {
provider: this.getName(),
model: this.model,
timestamp: new Date().toISOString()
}
};
}
/**
* Process and format the response
*/
protected handleResponse(
result: CompletionResult,
duration: number,
request: PreparedRequest
): AIResponse {
const inputTokens = result.inputTokens || this.calculateTokens(request.prompt);
const outputTokens = result.outputTokens || this.calculateTokens(result.content);
return {
content: result.content,
inputTokens,
outputTokens,
totalTokens: inputTokens + outputTokens,
model: result.model || this.model,
provider: this.getName(),
timestamp: request.metadata.timestamp,
duration,
finishReason: result.finishReason
};
}
/**
* Handle errors with proper wrapping
*/
protected handleError(error: unknown): never {
if (error instanceof TaskMasterError) {
throw error;
}
const errorMessage = error instanceof Error ? error.message : String(error);
const errorCode = this.getErrorCode(error);
throw new TaskMasterError(
`${this.getName()} provider error: ${errorMessage}`,
errorCode,
{
operation: 'generateCompletion',
resource: this.getName(),
details:
error instanceof Error
? {
name: error.name,
stack: error.stack,
model: this.model
}
: { error: String(error), model: this.model }
},
error instanceof Error ? error : undefined
);
}
/**
* Determine if request should be retried
*/
protected shouldRetry(error: unknown, attempt: number): boolean {
if (attempt >= this.getMaxRetries()) {
return false;
}
return this.isRetryableError(error);
}
/**
* Check if error is retryable
*/
protected isRetryableError(error: unknown): boolean {
if (this.isRateLimitError(error)) return true;
if (this.isTimeoutError(error)) return true;
if (this.isNetworkError(error)) return true;
return false;
}
/**
* Check if error is a rate limit error
*/
protected isRateLimitError(error: unknown): boolean {
if (error instanceof Error) {
const message = error.message.toLowerCase();
return (
message.includes('rate limit') ||
message.includes('too many requests') ||
message.includes('429')
);
}
return false;
}
/**
* Check if error is a timeout error
*/
protected isTimeoutError(error: unknown): boolean {
if (error instanceof Error) {
const message = error.message.toLowerCase();
return (
message.includes('timeout') ||
message.includes('timed out') ||
message.includes('econnreset')
);
}
return false;
}
/**
* Check if error is a network error
*/
protected isNetworkError(error: unknown): boolean {
if (error instanceof Error) {
const message = error.message.toLowerCase();
return (
message.includes('network') ||
message.includes('enotfound') ||
message.includes('econnrefused')
);
}
return false;
}
/**
* Calculate exponential backoff delay with jitter
*/
protected calculateBackoffDelay(attempt: number): number {
const exponentialDelay = BASE_RETRY_DELAY_MS * BACKOFF_MULTIPLIER ** (attempt - 1);
const clampedDelay = Math.min(exponentialDelay, MAX_RETRY_DELAY_MS);
// Add jitter to prevent thundering herd
const jitter = clampedDelay * JITTER_FACTOR * (Math.random() - 0.5) * 2;
return Math.round(clampedDelay + jitter);
}
/**
* Get error code from error
*/
protected getErrorCode(error: unknown): string {
if (this.isRateLimitError(error)) return ERROR_CODES.API_ERROR;
if (this.isTimeoutError(error)) return ERROR_CODES.NETWORK_ERROR;
if (this.isNetworkError(error)) return ERROR_CODES.NETWORK_ERROR;
if (error instanceof Error && error.message.includes('401')) {
return ERROR_CODES.AUTHENTICATION_ERROR;
}
return ERROR_CODES.PROVIDER_ERROR;
}
/**
* Sleep utility for delays
*/
protected sleep(ms: number): Promise<void> {
return new Promise((resolve) => setTimeout(resolve, ms));
}
/**
* Get default options for completions
*/
protected getDefaultOptions(): AIOptions {
return {
temperature: 0.7,
maxTokens: 2000,
topP: 1.0
};
}
/**
* Get maximum retry attempts
*/
protected getMaxRetries(): number {
return DEFAULT_MAX_RETRIES;
}
// Public interface methods
getModel(): string {
return this.model;
}
setModel(model: string): void {
this.model = model;
}
// Abstract methods that must be implemented by concrete providers
protected abstract generateCompletionInternal(
prompt: string,
options?: AIOptions
): Promise<CompletionResult>;
abstract calculateTokens(text: string, model?: string): number;
abstract getName(): string;
abstract getDefaultModel(): string;
// IAIProvider methods that must be implemented
abstract generateStreamingCompletion(
prompt: string,
options?: AIOptions
): AsyncIterator<Partial<AIResponse>>;
abstract isAvailable(): Promise<boolean>;
abstract getProviderInfo(): import('../../interfaces/ai-provider.interface.js').ProviderInfo;
abstract getAvailableModels(): import('../../interfaces/ai-provider.interface.js').AIModel[];
abstract validateCredentials(): Promise<boolean>;
abstract getUsageStats(): Promise<
import('../../interfaces/ai-provider.interface.js').ProviderUsageStats | null
>;
abstract initialize(): Promise<void>;
abstract close(): Promise<void>;
}

View File

@@ -0,0 +1,14 @@
/**
* @fileoverview Barrel export for AI provider modules
*/
export { BaseProvider } from './base-provider.js';
export type { BaseProviderConfig, CompletionResult } from './base-provider.js';
// Export provider factory when implemented
// export { ProviderFactory } from './provider-factory.js';
// Export concrete providers when implemented
// export { AnthropicProvider } from './adapters/anthropic-provider.js';
// export { OpenAIProvider } from './adapters/openai-provider.js';
// export { GoogleProvider } from './adapters/google-provider.js';

View File

@@ -3,11 +3,11 @@
* Provides common functionality and properties for all AI provider implementations
*/
import {
IAIProvider,
import type {
AIModel,
AIOptions,
AIResponse,
AIModel,
IAIProvider,
ProviderInfo,
ProviderUsageStats
} from '../interfaces/ai-provider.interface.js';
@@ -32,9 +32,9 @@ export abstract class BaseProvider implements IAIProvider {
/** Current model being used */
protected model: string;
/** Maximum number of retry attempts */
protected maxRetries: number = 3;
protected maxRetries = 3;
/** Delay between retries in milliseconds */
protected retryDelay: number = 1000;
protected retryDelay = 1000;
/**
* Constructor for BaseProvider
@@ -54,10 +54,7 @@ export abstract class BaseProvider implements IAIProvider {
}
// Abstract methods that concrete providers must implement
abstract generateCompletion(
prompt: string,
options?: AIOptions
): Promise<AIResponse>;
abstract generateCompletion(prompt: string, options?: AIOptions): Promise<AIResponse>;
abstract generateStreamingCompletion(
prompt: string,
options?: AIOptions

View File

@@ -1,27 +1,19 @@
/**
* @fileoverview AI provider implementations for the tm-core package
* This file exports all AI provider classes and interfaces
* @fileoverview Barrel export for provider modules
*/
// Provider interfaces and implementations
export * from './base-provider.js';
// export * from './anthropic-provider.js';
// export * from './openai-provider.js';
// export * from './perplexity-provider.js';
// Export AI providers from subdirectory
export { BaseProvider } from './ai/base-provider.js';
export type {
BaseProviderConfig,
CompletionResult
} from './ai/base-provider.js';
// Placeholder exports - these will be implemented in later tasks
export interface AIProvider {
name: string;
generateResponse(prompt: string): Promise<string>;
}
// Export all from AI module
export * from './ai/index.js';
/**
* @deprecated This is a placeholder class that will be properly implemented in later tasks
*/
export class PlaceholderProvider implements AIProvider {
name = 'placeholder';
// Storage providers will be exported here when implemented
// export * from './storage/index.js';
async generateResponse(prompt: string): Promise<string> {
return `Placeholder response for: ${prompt}`;
}
}
// Placeholder provider for tests
export { PlaceholderProvider } from './placeholder-provider.js';

View File

@@ -0,0 +1,15 @@
/**
* @fileoverview Placeholder provider for testing purposes
* @deprecated This is a placeholder implementation that will be replaced
*/
/**
* PlaceholderProvider for smoke tests
*/
export class PlaceholderProvider {
name = 'placeholder';
async generateResponse(prompt: string): Promise<string> {
return `Mock response to: ${prompt}`;
}
}

View File

@@ -2,8 +2,8 @@
* File-based storage implementation for Task Master
*/
import { promises as fs } from 'fs';
import path from 'path';
import { promises as fs } from 'node:fs';
import path from 'node:path';
import type { Task, TaskMetadata } from '../types/index.js';
import { BaseStorage, type StorageStats } from './storage.interface.js';
@@ -228,9 +228,7 @@ export class FileStorage extends BaseStorage {
/**
* Read and parse JSON file with error handling
*/
private async readJsonFile(
filePath: string
): Promise<FileStorageData | null> {
private async readJsonFile(filePath: string): Promise<FileStorageData | null> {
try {
const content = await fs.readFile(filePath, 'utf-8');
return JSON.parse(content);
@@ -248,10 +246,7 @@ export class FileStorage extends BaseStorage {
/**
* Write JSON file with atomic operation using temp file
*/
private async writeJsonFile(
filePath: string,
data: FileStorageData
): Promise<void> {
private async writeJsonFile(filePath: string, data: FileStorageData): Promise<void> {
// Use file locking to prevent concurrent writes
const lockKey = filePath;
const existingLock = this.fileLocks.get(lockKey);
@@ -273,10 +268,7 @@ export class FileStorage extends BaseStorage {
/**
* Perform the actual write operation
*/
private async performWrite(
filePath: string,
data: FileStorageData
): Promise<void> {
private async performWrite(filePath: string, data: FileStorageData): Promise<void> {
const tempPath = `${filePath}.tmp`;
try {
@@ -331,9 +323,7 @@ export class FileStorage extends BaseStorage {
try {
const files = await fs.readdir(dir);
const backupFiles = files
.filter(
(f) => f.startsWith(`${basename}.backup.`) && f.endsWith('.json')
)
.filter((f) => f.startsWith(`${basename}.backup.`) && f.endsWith('.json'))
.sort()
.reverse();

View File

@@ -2,12 +2,7 @@
* Storage interface and base implementation for Task Master
*/
import type {
Task,
TaskMetadata,
TaskFilter,
TaskSortOptions
} from '../types/index.js';
import type { Task, TaskFilter, TaskMetadata, TaskSortOptions } from '../types/index.js';
/**
* Storage statistics
@@ -38,11 +33,7 @@ export interface IStorage {
loadTasks(tag?: string): Promise<Task[]>;
saveTasks(tasks: Task[], tag?: string): Promise<void>;
appendTasks(tasks: Task[], tag?: string): Promise<void>;
updateTask(
taskId: string,
updates: Partial<Task>,
tag?: string
): Promise<boolean>;
updateTask(taskId: string, updates: Partial<Task>, tag?: string): Promise<boolean>;
deleteTask(taskId: string, tag?: string): Promise<boolean>;
exists(tag?: string): Promise<boolean>;
@@ -100,11 +91,7 @@ export abstract class BaseStorage implements IStorage {
await this.saveTasks(mergedTasks, tag);
}
async updateTask(
taskId: string,
updates: Partial<Task>,
tag?: string
): Promise<boolean> {
async updateTask(taskId: string, updates: Partial<Task>, tag?: string): Promise<boolean> {
const tasks = await this.loadTasks(tag);
const taskIndex = tasks.findIndex((t) => t.id === taskId);
@@ -149,7 +136,7 @@ export abstract class BaseStorage implements IStorage {
};
}
async saveMetadata(metadata: TaskMetadata, tag?: string): Promise<void> {
async saveMetadata(_metadata: TaskMetadata, _tag?: string): Promise<void> {
// Default implementation: metadata is derived from tasks
// Subclasses can override if they store metadata separately
}
@@ -189,26 +176,19 @@ export abstract class BaseStorage implements IStorage {
return tasks.filter((task) => {
// Status filter
if (filter.status) {
const statuses = Array.isArray(filter.status)
? filter.status
: [filter.status];
const statuses = Array.isArray(filter.status) ? filter.status : [filter.status];
if (!statuses.includes(task.status)) return false;
}
// Priority filter
if (filter.priority) {
const priorities = Array.isArray(filter.priority)
? filter.priority
: [filter.priority];
const priorities = Array.isArray(filter.priority) ? filter.priority : [filter.priority];
if (!priorities.includes(task.priority)) return false;
}
// Tags filter
if (filter.tags && filter.tags.length > 0) {
if (
!task.tags ||
!filter.tags.some((tag) => task.tags?.includes(tag))
) {
if (!task.tags || !filter.tags.some((tag) => task.tags?.includes(tag))) {
return false;
}
}
@@ -223,9 +203,7 @@ export abstract class BaseStorage implements IStorage {
if (filter.search) {
const searchLower = filter.search.toLowerCase();
const inTitle = task.title.toLowerCase().includes(searchLower);
const inDescription = task.description
.toLowerCase()
.includes(searchLower);
const inDescription = task.description.toLowerCase().includes(searchLower);
const inDetails = task.details.toLowerCase().includes(searchLower);
if (!inTitle && !inDescription && !inDetails) return false;
}
@@ -240,8 +218,7 @@ export abstract class BaseStorage implements IStorage {
const complexities = Array.isArray(filter.complexity)
? filter.complexity
: [filter.complexity];
if (!task.complexity || !complexities.includes(task.complexity))
return false;
if (!task.complexity || !complexities.includes(task.complexity)) return false;
}
return true;

View File

@@ -0,0 +1,302 @@
/**
* @fileoverview TaskMasterCore facade - main entry point for tm-core functionality
*/
import { TaskEntity } from './core/entities/task.entity.js';
import { ERROR_CODES, TaskMasterError } from './errors/task-master-error.js';
import type { IConfiguration } from './interfaces/configuration.interface.js';
import type { IStorage } from './interfaces/storage.interface.js';
import { FileStorage } from './storage/file-storage.js';
import type { Task, TaskFilter, TaskStatus } from './types/index.js';
/**
* Options for creating TaskMasterCore instance
*/
export interface TaskMasterCoreOptions {
projectPath: string;
configuration?: Partial<IConfiguration>;
storage?: IStorage;
}
/**
* List tasks result with metadata
*/
export interface ListTasksResult {
tasks: Task[];
total: number;
filtered: number;
tag?: string;
}
/**
* TaskMasterCore facade class
* Provides simplified API for all tm-core operations
*/
export class TaskMasterCore {
private storage: IStorage;
private projectPath: string;
private configuration: Partial<IConfiguration>;
private initialized = false;
constructor(options: TaskMasterCoreOptions) {
if (!options.projectPath) {
throw new TaskMasterError('Project path is required', ERROR_CODES.MISSING_CONFIGURATION);
}
this.projectPath = options.projectPath;
this.configuration = options.configuration || {};
// Use provided storage or create default FileStorage
this.storage = options.storage || new FileStorage(this.projectPath);
}
/**
* Initialize the TaskMasterCore instance
*/
async initialize(): Promise<void> {
if (this.initialized) return;
try {
await this.storage.initialize();
this.initialized = true;
} catch (error) {
throw new TaskMasterError(
'Failed to initialize TaskMasterCore',
ERROR_CODES.INTERNAL_ERROR,
{ operation: 'initialize' },
error as Error
);
}
}
/**
* Ensure the instance is initialized
*/
private async ensureInitialized(): Promise<void> {
if (!this.initialized) {
await this.initialize();
}
}
/**
* List all tasks with optional filtering
*/
async listTasks(options?: {
tag?: string;
filter?: TaskFilter;
includeSubtasks?: boolean;
}): Promise<ListTasksResult> {
await this.ensureInitialized();
try {
// Load tasks from storage
const rawTasks = await this.storage.loadTasks(options?.tag);
// Convert to TaskEntity for business logic
const taskEntities = TaskEntity.fromArray(rawTasks);
// Apply filters if provided
let filteredTasks = taskEntities;
if (options?.filter) {
filteredTasks = this.applyFilters(taskEntities, options.filter);
}
// Convert back to plain objects
const tasks = filteredTasks.map((entity) => entity.toJSON());
// Optionally exclude subtasks
const finalTasks =
options?.includeSubtasks === false
? tasks.map((task) => ({ ...task, subtasks: [] }))
: tasks;
return {
tasks: finalTasks,
total: rawTasks.length,
filtered: filteredTasks.length,
tag: options?.tag
};
} catch (error) {
throw new TaskMasterError(
'Failed to list tasks',
ERROR_CODES.INTERNAL_ERROR,
{
operation: 'listTasks',
tag: options?.tag
},
error as Error
);
}
}
/**
* Get a specific task by ID
*/
async getTask(taskId: string, tag?: string): Promise<Task | null> {
await this.ensureInitialized();
const result = await this.listTasks({ tag });
const task = result.tasks.find((t) => t.id === taskId);
return task || null;
}
/**
* Get tasks by status
*/
async getTasksByStatus(status: TaskStatus | TaskStatus[], tag?: string): Promise<Task[]> {
const statuses = Array.isArray(status) ? status : [status];
const result = await this.listTasks({
tag,
filter: { status: statuses }
});
return result.tasks;
}
/**
* Get task statistics
*/
async getTaskStats(tag?: string): Promise<{
total: number;
byStatus: Record<TaskStatus, number>;
withSubtasks: number;
blocked: number;
}> {
const result = await this.listTasks({ tag });
const stats = {
total: result.total,
byStatus: {} as Record<TaskStatus, number>,
withSubtasks: 0,
blocked: 0
};
// Initialize status counts
const statuses: TaskStatus[] = [
'pending',
'in-progress',
'done',
'deferred',
'cancelled',
'blocked',
'review'
];
statuses.forEach((status) => {
stats.byStatus[status] = 0;
});
// Count tasks
result.tasks.forEach((task) => {
stats.byStatus[task.status]++;
if (task.subtasks && task.subtasks.length > 0) {
stats.withSubtasks++;
}
if (task.status === 'blocked') {
stats.blocked++;
}
});
return stats;
}
/**
* Apply filters to tasks
*/
private applyFilters(tasks: TaskEntity[], filter: TaskFilter): TaskEntity[] {
return tasks.filter((task) => {
// Filter by status
if (filter.status) {
const statuses = Array.isArray(filter.status) ? filter.status : [filter.status];
if (!statuses.includes(task.status)) {
return false;
}
}
// Filter by priority
if (filter.priority) {
const priorities = Array.isArray(filter.priority) ? filter.priority : [filter.priority];
if (!priorities.includes(task.priority)) {
return false;
}
}
// Filter by tags
if (filter.tags && filter.tags.length > 0) {
if (!task.tags || !filter.tags.some((tag) => task.tags?.includes(tag))) {
return false;
}
}
// Filter by assignee
if (filter.assignee) {
if (task.assignee !== filter.assignee) {
return false;
}
}
// Filter by complexity
if (filter.complexity) {
const complexities = Array.isArray(filter.complexity)
? filter.complexity
: [filter.complexity];
if (!task.complexity || !complexities.includes(task.complexity)) {
return false;
}
}
// Filter by search term
if (filter.search) {
const searchLower = filter.search.toLowerCase();
const inTitle = task.title.toLowerCase().includes(searchLower);
const inDescription = task.description.toLowerCase().includes(searchLower);
const inDetails = task.details.toLowerCase().includes(searchLower);
if (!inTitle && !inDescription && !inDetails) {
return false;
}
}
// Filter by hasSubtasks
if (filter.hasSubtasks !== undefined) {
const hasSubtasks = task.subtasks.length > 0;
if (hasSubtasks !== filter.hasSubtasks) {
return false;
}
}
return true;
});
}
/**
* Close and cleanup resources
*/
async close(): Promise<void> {
if (this.storage) {
await this.storage.close();
}
this.initialized = false;
}
}
/**
* Factory function to create TaskMasterCore instance
*/
export function createTaskMasterCore(
projectPath: string,
options?: {
configuration?: Partial<IConfiguration>;
storage?: IStorage;
}
): TaskMasterCore {
return new TaskMasterCore({
projectPath,
configuration: options?.configuration,
storage: options?.storage
});
}

View File

@@ -93,10 +93,7 @@ export interface TaskCollection {
/**
* Type for creating a new task (without generated fields)
*/
export type CreateTask = Omit<
Task,
'id' | 'createdAt' | 'updatedAt' | 'subtasks'
> & {
export type CreateTask = Omit<Task, 'id' | 'createdAt' | 'updatedAt' | 'subtasks'> & {
subtasks?: Omit<Subtask, 'id' | 'parentId' | 'createdAt' | 'updatedAt'>[];
};
@@ -138,15 +135,7 @@ export interface TaskSortOptions {
export function isTaskStatus(value: unknown): value is TaskStatus {
return (
typeof value === 'string' &&
[
'pending',
'in-progress',
'done',
'deferred',
'cancelled',
'blocked',
'review'
].includes(value)
['pending', 'in-progress', 'done', 'deferred', 'cancelled', 'blocked', 'review'].includes(value)
);
}
@@ -154,10 +143,7 @@ export function isTaskStatus(value: unknown): value is TaskStatus {
* Type guard to check if a value is a valid TaskPriority
*/
export function isTaskPriority(value: unknown): value is TaskPriority {
return (
typeof value === 'string' &&
['low', 'medium', 'high', 'critical'].includes(value)
);
return typeof value === 'string' && ['low', 'medium', 'high', 'critical'].includes(value);
}
/**
@@ -165,8 +151,7 @@ export function isTaskPriority(value: unknown): value is TaskPriority {
*/
export function isTaskComplexity(value: unknown): value is TaskComplexity {
return (
typeof value === 'string' &&
['simple', 'moderate', 'complex', 'very-complex'].includes(value)
typeof value === 'string' && ['simple', 'moderate', 'complex', 'very-complex'].includes(value)
);
}

View File

@@ -3,7 +3,7 @@
* Provides functions to generate unique identifiers for tasks and subtasks
*/
import { randomBytes } from 'crypto';
import { randomBytes } from 'node:crypto';
/**
* Generates a unique task ID using the format: TASK-{timestamp}-{random}
@@ -33,28 +33,22 @@ export function generateTaskId(): string {
* // Returns: "TASK-123-A7B3.2"
* ```
*/
export function generateSubtaskId(
parentId: string,
existingSubtasks: string[] = []
): string {
export function generateSubtaskId(parentId: string, existingSubtasks: string[] = []): string {
// Find existing subtasks for this parent
const parentSubtasks = existingSubtasks.filter((id) =>
id.startsWith(`${parentId}.`)
);
const parentSubtasks = existingSubtasks.filter((id) => id.startsWith(`${parentId}.`));
// Extract sequential numbers and find the highest
const sequentialNumbers = parentSubtasks
.map((id) => {
const parts = id.split('.');
const lastPart = parts[parts.length - 1];
return parseInt(lastPart, 10);
return Number.parseInt(lastPart, 10);
})
.filter((num) => !isNaN(num))
.filter((num) => !Number.isNaN(num))
.sort((a, b) => a - b);
// Determine the next sequential number
const nextSequential =
sequentialNumbers.length > 0 ? Math.max(...sequentialNumbers) + 1 : 1;
const nextSequential = sequentialNumbers.length > 0 ? Math.max(...sequentialNumbers) + 1 : 1;
return `${parentId}.${nextSequential}`;
}
@@ -118,8 +112,8 @@ export function isValidSubtaskId(id: string): boolean {
// Remaining parts should be positive integers
const sequentialParts = parts.slice(1);
return sequentialParts.every((part) => {
const num = parseInt(part, 10);
return !isNaN(num) && num > 0 && part === num.toString();
const num = Number.parseInt(part, 10);
return !Number.isNaN(num) && num > 0 && part === num.toString();
});
}