feat: implement cross-tag task movement functionality (#1088)

* feat: enhance move command with cross-tag functionality

- Updated the `move` command to allow moving tasks between different tags, including options for handling dependencies.
- Added new options: `--from-tag`, `--to-tag`, `--with-dependencies`, `--ignore-dependencies`, and `--force`.
- Implemented validation for cross-tag moves and dependency checks.
- Introduced helper functions in the dependency manager for validating and resolving cross-tag dependencies.
- Added integration and unit tests to cover new functionality and edge cases.

* fix: refactor cross-tag move logic and enhance validation

- Moved the import of `moveTasksBetweenTags` to the correct location in `commands.js` for better clarity.
- Added new helper functions in `dependency-manager.js` to improve validation and error handling for cross-tag moves.
- Enhanced existing functions to ensure proper handling of task dependencies and conflicts.
- Updated tests to cover new validation scenarios and ensure robust error messaging for invalid task IDs and tags.

* fix: improve task ID handling and error messaging in cross-tag moves

- Refactored `moveTasksBetweenTags` to normalize task IDs for comparison, ensuring consistent handling of string and numeric IDs.
- Enhanced error messages for cases where source and target tags are the same but no destination is specified.
- Updated tests to validate new behavior, including handling string dependencies correctly during cross-tag moves.
- Cleaned up existing code for better readability and maintainability.

* test: add comprehensive tests for cross-tag move and dependency validation

- Introduced new test files for `move-cross-tag` and `cross-tag-dependencies` to cover various scenarios in cross-tag task movement.
- Implemented tests for handling task movement with and without dependencies, including edge cases for error handling.
- Enhanced existing tests in `fix-dependencies-command` and `move-task` to ensure robust validation of task IDs and dependencies.
- Mocked necessary modules and functions to isolate tests and improve reliability.
- Ensured coverage for both successful and failed cross-tag move operations, validating expected outcomes and error messages.

* test: refactor cross-tag move tests for better clarity and reusability

- Introduced a helper function `simulateCrossTagMove` to streamline cross-tag move test cases, reducing redundancy and improving readability.
- Updated existing tests to utilize the new helper function, ensuring consistent handling of expected messages and options.
- Enhanced test coverage for various scenarios, including handling of dependencies and flags.

* feat: add cross-tag task movement functionality

- Introduced new commands for moving tasks between different tags, enhancing project organization capabilities.
- Updated README with usage examples for cross-tag movement, including options for handling dependencies.
- Created comprehensive documentation for cross-tag task movement, detailing usage, error handling, and best practices.
- Implemented core logic for cross-tag moves, including validation for dependencies and error handling.
- Added integration and unit tests to ensure robust functionality and coverage for various scenarios, including edge cases.

* fix: enhance error handling and logging in cross-tag task movement

- Improved logging in `moveTaskCrossTagDirect` to include detailed arguments for better traceability.
- Refactored error handling to utilize structured error objects, providing clearer suggestions for resolving cross-tag dependency conflicts and subtask movement restrictions.
- Updated documentation to reflect changes in error handling and provide clearer guidance on task movement options.
- Added integration tests for cross-tag movement scenarios, ensuring robust validation of error handling and task movement logic.
- Cleaned up existing tests for clarity and reusability, enhancing overall test coverage.

* feat: enhance dependency resolution and error handling in task movement

- Added recursive dependency resolution for tasks in `moveTasksBetweenTags`, improving handling of complex task relationships.
- Introduced helper functions to find all dependencies and reverse dependencies, ensuring comprehensive coverage during task moves.
- Enhanced error messages in `validateSubtaskMove` and `displaySubtaskMoveError` for better clarity on movement restrictions.
- Updated tests to cover new functionality, including integration tests for complex cross-tag movement scenarios and edge cases.
- Refactored existing code for improved readability and maintainability, ensuring consistent handling of task IDs and dependencies.

* feat: unify dependency traversal and enhance task management utilities

- Introduced `traverseDependencies` utility for unified forward and reverse dependency traversal, improving code reusability and clarity.
- Refactored `findAllDependenciesRecursively` to leverage the new utility, streamlining dependency resolution in task management.
- Added `formatTaskIdForDisplay` helper for better task ID formatting in UI, enhancing user experience during error displays.
- Updated tests to cover new utility functions and ensure robust validation of dependency handling across various scenarios.
- Improved overall code organization and readability, ensuring consistent handling of task dependencies and IDs.

* fix: improve validation for dependency parameters in `findAllDependenciesRecursively`

- Added checks to ensure `sourceTasks` and `allTasks` are arrays, throwing errors if not, to prevent runtime issues.
- Updated documentation comment for clarity on the function's purpose and parameters.

* fix: remove `force` option from task movement parameters

- Eliminated the `force` parameter from the `moveTaskCrossTagDirect` function and related tools, simplifying the task movement logic.
- Updated documentation and tests to reflect the removal of the `force` option, ensuring clarity and consistency across the codebase.
- Adjusted related functions and tests to focus on `ignoreDependencies` as the primary control for handling dependency conflicts during task moves.

* Add cross-tag task movement functionality

- Introduced functionality for organizing tasks across different contexts by enabling cross-tag movement.
- Added `formatTaskIdForDisplay` helper to improve task ID formatting in UI error messages.
- Updated relevant tests to incorporate new functionality and ensure accurate error displays during task movements.

* Update scripts/modules/dependency-manager.js

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>

* refactor(dependency-manager): Fix subtask resolution and extract helper functions

1. Fix subtask finding logic (lines 1315-1330):
   - Correctly locate parent task by numeric ID
   - Search within parent's subtasks array instead of top-level tasks
   - Properly handle relative subtask references

2. Extract helper functions from getDependentTaskIds (lines 1440-1636):
   - Move findTasksThatDependOn as module-level function
   - Move taskDependsOnSource as module-level function
   - Move subtasksDependOnSource as module-level function
   - Improves readability, maintainability, and testability

Both fixes address architectural issues and improve code organization.

* refactor(dependency-manager): Enhance subtask resolution and dependency validation

- Improved subtask resolution logic to correctly find parent tasks and their subtasks, ensuring accurate identification of dependencies.
- Filtered out null/undefined dependencies before processing, enhancing robustness in dependency checks.
- Updated comments for clarity on the logic flow and purpose of changes, improving code maintainability.

* refactor(move-task): clarify destination ID description and improve skipped task handling

- Updated the description for the destination ID to clarify its usage in cross-tag moves.
- Simplified the handling of skipped tasks during multiple task movements, improving readability and logging.
- Enhanced the API result response to include detailed information about moved and skipped tasks, ensuring better feedback for users.

* refactor(commands): remove redundant tag validation logic

- Eliminated the check for identical source and target tags in the task movement logic, simplifying the code.
- This change streamlines the flow for within-tag moves, enhancing readability and maintainability.

* refactor(commands): enhance move command logic and error handling

- Introduced helper functions for better organization of cross-tag and within-tag move logic, improving code readability and maintainability.
- Enhanced error handling with structured error objects, providing clearer feedback for dependency conflicts and invalid tag combinations.
- Updated move command help output to include best practices and error resolution tips, ensuring users have comprehensive guidance during task movements.
- Streamlined task movement logic to handle multiple tasks more effectively, including detailed logging of successful and failed moves.

* test(dependency-manager): add subtasks to task structure and mock dependency traversal

- Updated `circular-dependencies.test.js` to include subtasks in task definitions, enhancing test coverage for task structures with nested dependencies.
- Mocked `traverseDependencies` in `fix-dependencies-command.test.js` to ensure consistent behavior during tests, improving reliability of dependency-related tests.

* refactor(dependency-manager): extract subtask finding logic into helper function

- Added `findSubtaskInParent` function to encapsulate subtask resolution within a parent task's subtasks array, improving code organization and readability.
- Updated `findDependencyTask` to utilize the new helper function, streamlining the logic for finding subtasks and enhancing maintainability.
- Enhanced comments for clarity on the purpose and functionality of the new subtask finding logic.

* refactor(ui): enhance subtask ID validation and improve error handling

- Added validation for subtask ID format in `formatDependenciesWithStatus` and `taskExists`, ensuring proper handling of invalid formats.
- Updated error logging in `displaySubtaskMoveError` to provide warnings for unexpected task ID formats, improving user feedback.
- Converted hints to a Set in `displayDependencyValidationHints` to ensure unique hints are displayed, enhancing clarity in the UI.

* test(cli): remove redundant timing check in complex cross-tag scenarios

- Eliminated the timing check for task completion within 5 seconds in `complex-cross-tag-scenarios.test.js`, streamlining the test logic.
- This change focuses on verifying task success without unnecessary timing constraints, enhancing test clarity and maintainability.

* test(integration): enhance task movement tests with mock file system

- Added integration tests for moving tasks within the same tag and between different tags using the actual `moveTask` and `moveTasksBetweenTags` functions.
- Implemented `mock-fs` to simulate file system interactions, improving test isolation and reliability.
- Verified task movement success and ensured proper handling of subtasks and dependencies, enhancing overall test coverage for task management functionality.
- Included error handling tests for missing tags and task IDs to ensure robustness in task movement operations.

* test(unit): add comprehensive tests for moveTaskCrossTagDirect functionality

- Introduced new test cases to verify mock functionality, ensuring that mocks for `findTasksPath` and `readJSON` are working as expected.
- Added tests for parameter validation, error handling, and function call flow, including scenarios for missing project roots and identical source/target tags.
- Enhanced coverage for ID parsing and move options, ensuring robust handling of various input conditions and improving overall test reliability.

* test(integration): skip tests for dependency conflict handling and withDependencies option

- Marked tests for handling dependency conflicts and the withDependencies option as skipped due to issues with the mock setup.
- Added TODOs to address the mock-fs setup for complex dependency scenarios, ensuring future improvements in test reliability.

* test(unit): expand cross-tag move command tests with comprehensive mocks

- Added extensive mocks for various modules to enhance the testing of the cross-tag move functionality in `move-cross-tag.test.js`.
- Implemented detailed test cases for handling cross-tag moves, including validation for missing parameters and identical source/target tags.
- Improved error handling tests to ensure robust feedback for invalid operations, enhancing overall test reliability and coverage.

* test(integration): add complex dependency scenarios to task movement tests

- Introduced new integration tests for handling complex dependency scenarios in task movement, utilizing the actual `moveTasksBetweenTags` function.
- Added tests for circular dependencies, nested dependency chains, and cross-tag dependency resolution, enhancing coverage and reliability.
- Documented limitations of the mock-fs setup for complex scenarios and provided warnings in the test output to guide future improvements.
- Skipped tests for dependency conflicts and the withDependencies option due to mock setup issues, with TODOs for resolution.

* test(unit): refactor move-cross-tag tests with focused mock system

- Simplified mocking in `move-cross-tag.test.js` by implementing a configuration-driven mock system, reducing the number of mocked modules from 20+ to 5 core functionalities.
- Introduced a reusable mock factory to streamline the creation of mocks based on configuration, enhancing maintainability and clarity.
- Added documentation for the new mock system, detailing usage examples and benefits, including reduced complexity and improved test focus.
- Implemented tests to validate the mock configuration, ensuring flexibility in enabling/disabling specific mocks.

* test(unit): clean up mocks and improve isEmpty function in fix-dependencies-command tests

- Removed the mock for `traverseDependencies` as it was unnecessary, simplifying the test setup.
- Updated the `isEmpty` function to clarify its behavior regarding null and undefined values, enhancing code readability and maintainability.

* test(unit): update traverseDependencies mock for consistency across tests

- Standardized the mock implementation of `traverseDependencies` in both `fix-dependencies-command.test.js` and `complexity-report-tag-isolation.test.js` to accept `sourceTasks`, `allTasks`, and `options` parameters, ensuring uniformity in test setups.
- This change enhances clarity and maintainability of the tests by aligning the mock behavior across different test files.

* fix(core): improve task movement error handling and ID normalization

- Wrapped task movement logic in a try-finally block to ensure console output is restored even on errors, enhancing reliability.
- Normalized source IDs to handle mixed string/number comparisons, preventing potential issues in dependency checks.
- Added tests for ID type consistency to verify that the normalization fix works correctly across various scenarios, improving test coverage and robustness.

* refactor(task-manager): restructure task movement logic for improved validation and execution

- Renamed and refactored `moveTasksBetweenTags` to streamline the task movement process into distinct phases: validation, data preparation, dependency resolution, execution, and finalization.
- Introduced `validateMove`, `prepareTaskData`, `resolveDependencies`, `executeMoveOperation`, and `finalizeMove` functions to enhance modularity and clarity.
- Updated documentation comments to reflect changes in function responsibilities and parameters.
- Added comprehensive unit tests for the new structure, ensuring robust validation and error handling across various scenarios.
- Improved handling of dependencies and task existence checks during the move operation, enhancing overall reliability.

* fix(move-task): streamline task movement logic and improve error handling

- Refactored the task movement process to enhance clarity and maintainability by replacing `forEach` with a `for...of` loop for better async handling.
- Consolidated error handling and result logging to ensure consistent feedback during task moves.
- Updated the logic for generating files only on the last move, improving performance and reducing unnecessary operations.
- Enhanced validation for skipped tasks, ensuring accurate reporting of moved and skipped tasks in the final result.

* fix(docs): update error message formatting and enhance clarity in task movement documentation

- Changed code block syntax from generic to `text` for better readability in error messages related to task movement and dependency conflicts.
- Ensured consistent formatting across all error message examples to improve user understanding of task movement restrictions and resolutions.
- Added a newline at the end of the file for proper formatting.

* Update .changeset/crazy-meals-hope.md

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>

* chore: improve changeset

* chore: improve changeset

* fix referenced bug in docs and remove docs

* chore: fix format

---------

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
Co-authored-by: Ralph Khreish <35776126+Crunchyman-ralph@users.noreply.github.com>
This commit is contained in:
Parthy
2025-08-11 18:58:51 +02:00
committed by GitHub
parent 782728ff95
commit 04e11b5e82
31 changed files with 8301 additions and 167 deletions

View File

@@ -14,12 +14,35 @@ import {
taskExists,
formatTaskId,
findCycles,
traverseDependencies,
isSilentMode
} from './utils.js';
import { displayBanner } from './ui.js';
import { generateTaskFiles } from './task-manager.js';
import generateTaskFiles from './task-manager/generate-task-files.js';
/**
* Structured error class for dependency operations
*/
class DependencyError extends Error {
constructor(code, message, data = {}) {
super(message);
this.name = 'DependencyError';
this.code = code;
this.data = data;
}
}
/**
* Error codes for dependency operations
*/
const DEPENDENCY_ERROR_CODES = {
CANNOT_MOVE_SUBTASK: 'CANNOT_MOVE_SUBTASK',
INVALID_TASK_ID: 'INVALID_TASK_ID',
INVALID_SOURCE_TAG: 'INVALID_SOURCE_TAG',
INVALID_TARGET_TAG: 'INVALID_TARGET_TAG'
};
/**
* Add a dependency to a task
@@ -1235,6 +1258,580 @@ function validateAndFixDependencies(
return changesDetected;
}
/**
* Recursively find all dependencies for a set of tasks with depth limiting
* Recursively find all dependencies for a set of tasks with depth limiting
*
* @note This function depends on the traverseDependencies utility from utils.js
* for the actual dependency traversal logic.
*
* @param {Array} sourceTasks - Array of source tasks to find dependencies for
* @param {Array} allTasks - Array of all available tasks
* @param {Object} options - Options object
* @param {number} options.maxDepth - Maximum recursion depth (default: 50)
* @param {boolean} options.includeSelf - Whether to include self-references (default: false)
* @returns {Array} Array of all dependency task IDs
*/
function findAllDependenciesRecursively(sourceTasks, allTasks, options = {}) {
if (!Array.isArray(sourceTasks)) {
throw new Error('Source tasks parameter must be an array');
}
if (!Array.isArray(allTasks)) {
throw new Error('All tasks parameter must be an array');
}
return traverseDependencies(sourceTasks, allTasks, {
...options,
direction: 'forward',
logger: { warn: log.warn || console.warn }
});
}
/**
* Find dependency task by ID, handling various ID formats
* @param {string|number} depId - Dependency ID to find
* @param {string} taskId - ID of the task that has this dependency
* @param {Array} allTasks - Array of all tasks to search
* @returns {Object|null} Found dependency task or null
*/
/**
* Find a subtask within a parent task's subtasks array
* @param {string} parentId - The parent task ID
* @param {string|number} subtaskId - The subtask ID to find
* @param {Array} allTasks - Array of all tasks to search in
* @param {boolean} useStringComparison - Whether to use string comparison for subtaskId
* @returns {Object|null} The found subtask with full ID or null if not found
*/
function findSubtaskInParent(
parentId,
subtaskId,
allTasks,
useStringComparison = false
) {
// Convert parentId to numeric for proper comparison with top-level task IDs
const numericParentId = parseInt(parentId, 10);
const parentTask = allTasks.find((t) => t.id === numericParentId);
if (parentTask && parentTask.subtasks && Array.isArray(parentTask.subtasks)) {
const foundSubtask = parentTask.subtasks.find((subtask) =>
useStringComparison
? String(subtask.id) === String(subtaskId)
: subtask.id === subtaskId
);
if (foundSubtask) {
// Return a task-like object that represents the subtask with full ID
return {
...foundSubtask,
id: `${parentId}.${foundSubtask.id}`
};
}
}
return null;
}
function findDependencyTask(depId, taskId, allTasks) {
if (!depId) {
return null;
}
// Convert depId to string for consistent comparison
const depIdStr = String(depId);
// Find the dependency task - handle both top-level and subtask IDs
let depTask = null;
// First try exact match (for top-level tasks)
depTask = allTasks.find((t) => String(t.id) === depIdStr);
// If not found and it's a subtask reference (contains dot), find the parent task first
if (!depTask && depIdStr.includes('.')) {
const [parentId, subtaskId] = depIdStr.split('.');
depTask = findSubtaskInParent(parentId, subtaskId, allTasks, true);
}
// If still not found, try numeric comparison for relative subtask references
if (!depTask && !isNaN(depId)) {
const numericId = parseInt(depId, 10);
// For subtasks, this might be a relative reference within the same parent
if (taskId && typeof taskId === 'string' && taskId.includes('.')) {
const [parentId] = taskId.split('.');
depTask = findSubtaskInParent(parentId, numericId, allTasks, false);
}
}
return depTask;
}
/**
* Check if a task has cross-tag dependencies
* @param {Object} task - Task to check
* @param {string} targetTag - Target tag name
* @param {Array} allTasks - Array of all tasks from all tags
* @returns {Array} Array of cross-tag dependency conflicts
*/
function findTaskCrossTagConflicts(task, targetTag, allTasks) {
const conflicts = [];
// Validate task.dependencies is an array before processing
if (!Array.isArray(task.dependencies) || task.dependencies.length === 0) {
return conflicts;
}
// Filter out null/undefined dependencies and check each valid dependency
const validDependencies = task.dependencies.filter((depId) => depId != null);
validDependencies.forEach((depId) => {
const depTask = findDependencyTask(depId, task.id, allTasks);
if (depTask && depTask.tag !== targetTag) {
conflicts.push({
taskId: task.id,
dependencyId: depId,
dependencyTag: depTask.tag,
message: `Task ${task.id} depends on ${depId} (in ${depTask.tag})`
});
}
});
return conflicts;
}
function validateCrossTagMove(task, sourceTag, targetTag, allTasks) {
// Parameter validation
if (!task || typeof task !== 'object') {
throw new Error('Task parameter must be a valid object');
}
if (!sourceTag || typeof sourceTag !== 'string') {
throw new Error('Source tag must be a valid string');
}
if (!targetTag || typeof targetTag !== 'string') {
throw new Error('Target tag must be a valid string');
}
if (!Array.isArray(allTasks)) {
throw new Error('All tasks parameter must be an array');
}
const conflicts = findTaskCrossTagConflicts(task, targetTag, allTasks);
return {
canMove: conflicts.length === 0,
conflicts
};
}
/**
* Find all cross-tag dependencies for a set of tasks
* @param {Array} sourceTasks - Array of tasks to check
* @param {string} sourceTag - Source tag name
* @param {string} targetTag - Target tag name
* @param {Array} allTasks - Array of all tasks from all tags
* @returns {Array} Array of cross-tag dependency conflicts
*/
function findCrossTagDependencies(sourceTasks, sourceTag, targetTag, allTasks) {
// Parameter validation
if (!Array.isArray(sourceTasks)) {
throw new Error('Source tasks parameter must be an array');
}
if (!sourceTag || typeof sourceTag !== 'string') {
throw new Error('Source tag must be a valid string');
}
if (!targetTag || typeof targetTag !== 'string') {
throw new Error('Target tag must be a valid string');
}
if (!Array.isArray(allTasks)) {
throw new Error('All tasks parameter must be an array');
}
const conflicts = [];
sourceTasks.forEach((task) => {
// Validate task object and dependencies array
if (
!task ||
typeof task !== 'object' ||
!Array.isArray(task.dependencies) ||
task.dependencies.length === 0
) {
return;
}
// Use the shared helper function to find conflicts for this task
const taskConflicts = findTaskCrossTagConflicts(task, targetTag, allTasks);
conflicts.push(...taskConflicts);
});
return conflicts;
}
/**
* Helper function to find all tasks that depend on a given task (reverse dependencies)
* @param {string|number} taskId - The task ID to find dependencies for
* @param {Array} allTasks - Array of all tasks to search
* @param {Set} dependentTaskIds - Set to add found dependencies to
*/
function findTasksThatDependOn(taskId, allTasks, dependentTaskIds) {
// Find the task object for the given ID
const sourceTask = allTasks.find((t) => t.id === taskId);
if (!sourceTask) {
return;
}
// Use the shared utility for reverse dependency traversal
const reverseDeps = traverseDependencies([sourceTask], allTasks, {
direction: 'reverse',
includeSelf: false,
logger: { warn: log.warn || console.warn }
});
// Add all found reverse dependencies to the dependentTaskIds set
reverseDeps.forEach((depId) => dependentTaskIds.add(depId));
}
/**
* Helper function to check if a task depends on a source task
* @param {Object} task - Task to check for dependencies
* @param {Object} sourceTask - Source task to check dependency against
* @returns {boolean} True if task depends on source task
*/
function taskDependsOnSource(task, sourceTask) {
if (!task || !Array.isArray(task.dependencies)) {
return false;
}
const sourceTaskIdStr = String(sourceTask.id);
return task.dependencies.some((depId) => {
if (!depId) return false;
const depIdStr = String(depId);
// Exact match
if (depIdStr === sourceTaskIdStr) {
return true;
}
// Handle subtask references
if (
sourceTaskIdStr &&
typeof sourceTaskIdStr === 'string' &&
sourceTaskIdStr.includes('.')
) {
// If source is a subtask, check if dependency references the parent
const [parentId] = sourceTaskIdStr.split('.');
if (depIdStr === parentId) {
return true;
}
}
// Handle relative subtask references
if (
depIdStr &&
typeof depIdStr === 'string' &&
depIdStr.includes('.') &&
sourceTaskIdStr &&
typeof sourceTaskIdStr === 'string' &&
sourceTaskIdStr.includes('.')
) {
const [depParentId] = depIdStr.split('.');
const [sourceParentId] = sourceTaskIdStr.split('.');
if (depParentId === sourceParentId) {
// Both are subtasks of the same parent, check if they reference each other
const depSubtaskNum = parseInt(depIdStr.split('.')[1], 10);
const sourceSubtaskNum = parseInt(sourceTaskIdStr.split('.')[1], 10);
if (depSubtaskNum === sourceSubtaskNum) {
return true;
}
}
}
return false;
});
}
/**
* Helper function to check if any subtasks of a task depend on source tasks
* @param {Object} task - Task to check subtasks of
* @param {Array} sourceTasks - Array of source tasks to check dependencies against
* @returns {boolean} True if any subtasks depend on source tasks
*/
function subtasksDependOnSource(task, sourceTasks) {
if (!task.subtasks || !Array.isArray(task.subtasks)) {
return false;
}
return task.subtasks.some((subtask) => {
// Check if this subtask depends on any source task
const subtaskDependsOnSource = sourceTasks.some((sourceTask) =>
taskDependsOnSource(subtask, sourceTask)
);
if (subtaskDependsOnSource) {
return true;
}
// Recursively check if any nested subtasks depend on source tasks
if (subtask.subtasks && Array.isArray(subtask.subtasks)) {
return subtasksDependOnSource(subtask, sourceTasks);
}
return false;
});
}
/**
* Get all dependent task IDs for a set of cross-tag dependencies
* @param {Array} sourceTasks - Array of source tasks
* @param {Array} crossTagDependencies - Array of cross-tag dependency conflicts
* @param {Array} allTasks - Array of all tasks from all tags
* @returns {Array} Array of dependent task IDs to move
*/
function getDependentTaskIds(sourceTasks, crossTagDependencies, allTasks) {
// Enhanced parameter validation
if (!Array.isArray(sourceTasks)) {
throw new Error('Source tasks parameter must be an array');
}
if (!Array.isArray(crossTagDependencies)) {
throw new Error('Cross tag dependencies parameter must be an array');
}
if (!Array.isArray(allTasks)) {
throw new Error('All tasks parameter must be an array');
}
// Use the shared recursive dependency finder
const dependentTaskIds = new Set(
findAllDependenciesRecursively(sourceTasks, allTasks, {
includeSelf: false
})
);
// Add immediate dependency IDs from conflicts and find their dependencies recursively
const conflictTasksToProcess = [];
crossTagDependencies.forEach((conflict) => {
if (conflict && conflict.dependencyId) {
const depId =
typeof conflict.dependencyId === 'string'
? parseInt(conflict.dependencyId, 10)
: conflict.dependencyId;
if (!isNaN(depId)) {
dependentTaskIds.add(depId);
// Find the task object for recursive dependency finding
const depTask = allTasks.find((t) => t.id === depId);
if (depTask) {
conflictTasksToProcess.push(depTask);
}
}
}
});
// Find dependencies of conflict tasks
if (conflictTasksToProcess.length > 0) {
const conflictDependencies = findAllDependenciesRecursively(
conflictTasksToProcess,
allTasks,
{ includeSelf: false }
);
conflictDependencies.forEach((depId) => dependentTaskIds.add(depId));
}
// For --with-dependencies, we also need to find all dependencies of the source tasks
sourceTasks.forEach((sourceTask) => {
if (sourceTask && sourceTask.id) {
// Find all tasks that this source task depends on (forward dependencies) - already handled above
// Find all tasks that depend on this source task (reverse dependencies)
findTasksThatDependOn(sourceTask.id, allTasks, dependentTaskIds);
}
});
// Also include any tasks that depend on the source tasks
sourceTasks.forEach((sourceTask) => {
if (!sourceTask || typeof sourceTask !== 'object' || !sourceTask.id) {
return; // Skip invalid source tasks
}
allTasks.forEach((task) => {
// Validate task and dependencies array
if (
!task ||
typeof task !== 'object' ||
!Array.isArray(task.dependencies)
) {
return;
}
// Check if this task depends on the source task
const hasDependency = taskDependsOnSource(task, sourceTask);
// Check if any subtasks of this task depend on the source task
const subtasksHaveDependency = subtasksDependOnSource(task, [sourceTask]);
if (hasDependency || subtasksHaveDependency) {
dependentTaskIds.add(task.id);
}
});
});
return Array.from(dependentTaskIds);
}
/**
* Validate subtask movement - block direct cross-tag subtask moves
* @param {string} taskId - Task ID to validate
* @param {string} sourceTag - Source tag name
* @param {string} targetTag - Target tag name
* @throws {Error} If subtask movement is attempted
*/
function validateSubtaskMove(taskId, sourceTag, targetTag) {
// Parameter validation
if (!taskId || typeof taskId !== 'string') {
throw new DependencyError(
DEPENDENCY_ERROR_CODES.INVALID_TASK_ID,
'Task ID must be a valid string'
);
}
if (!sourceTag || typeof sourceTag !== 'string') {
throw new DependencyError(
DEPENDENCY_ERROR_CODES.INVALID_SOURCE_TAG,
'Source tag must be a valid string'
);
}
if (!targetTag || typeof targetTag !== 'string') {
throw new DependencyError(
DEPENDENCY_ERROR_CODES.INVALID_TARGET_TAG,
'Target tag must be a valid string'
);
}
if (taskId.includes('.')) {
throw new DependencyError(
DEPENDENCY_ERROR_CODES.CANNOT_MOVE_SUBTASK,
`Cannot move subtask ${taskId} directly between tags.
First promote it to a full task using:
task-master remove-subtask --id=${taskId} --convert`,
{
taskId,
sourceTag,
targetTag
}
);
}
}
/**
* Check if a task can be moved with its dependencies
* @param {string} taskId - Task ID to check
* @param {string} sourceTag - Source tag name
* @param {string} targetTag - Target tag name
* @param {Array} allTasks - Array of all tasks from all tags
* @returns {Object} Object with canMove boolean and dependentTaskIds array
*/
function canMoveWithDependencies(taskId, sourceTag, targetTag, allTasks) {
// Parameter validation
if (!taskId || typeof taskId !== 'string') {
throw new Error('Task ID must be a valid string');
}
if (!sourceTag || typeof sourceTag !== 'string') {
throw new Error('Source tag must be a valid string');
}
if (!targetTag || typeof targetTag !== 'string') {
throw new Error('Target tag must be a valid string');
}
if (!Array.isArray(allTasks)) {
throw new Error('All tasks parameter must be an array');
}
// Enhanced task lookup to handle subtasks properly
let sourceTask = null;
// Check if it's a subtask ID (e.g., "1.2")
if (taskId.includes('.')) {
const [parentId, subtaskId] = taskId
.split('.')
.map((id) => parseInt(id, 10));
const parentTask = allTasks.find(
(t) => t.id === parentId && t.tag === sourceTag
);
if (
parentTask &&
parentTask.subtasks &&
Array.isArray(parentTask.subtasks)
) {
const subtask = parentTask.subtasks.find((st) => st.id === subtaskId);
if (subtask) {
// Create a copy of the subtask with parent context
sourceTask = {
...subtask,
parentTask: {
id: parentTask.id,
title: parentTask.title,
status: parentTask.status
},
isSubtask: true
};
}
}
} else {
// Regular task lookup - handle both string and numeric IDs
sourceTask = allTasks.find((t) => {
const taskIdNum = parseInt(taskId, 10);
return (t.id === taskIdNum || t.id === taskId) && t.tag === sourceTag;
});
}
if (!sourceTask) {
return {
canMove: false,
dependentTaskIds: [],
conflicts: [],
error: 'Task not found'
};
}
const validation = validateCrossTagMove(
sourceTask,
sourceTag,
targetTag,
allTasks
);
// Fix contradictory logic: return canMove: false when conflicts exist
if (validation.canMove) {
return {
canMove: true,
dependentTaskIds: [],
conflicts: []
};
}
// When conflicts exist, return canMove: false with conflicts and dependent task IDs
const dependentTaskIds = getDependentTaskIds(
[sourceTask],
validation.conflicts,
allTasks
);
return {
canMove: false,
dependentTaskIds,
conflicts: validation.conflicts
};
}
export {
addDependency,
removeDependency,
@@ -1245,5 +1842,15 @@ export {
removeDuplicateDependencies,
cleanupSubtaskDependencies,
ensureAtLeastOneIndependentSubtask,
validateAndFixDependencies
validateAndFixDependencies,
findDependencyTask,
findTaskCrossTagConflicts,
validateCrossTagMove,
findCrossTagDependencies,
getDependentTaskIds,
validateSubtaskMove,
canMoveWithDependencies,
findAllDependenciesRecursively,
DependencyError,
DEPENDENCY_ERROR_CODES
};