224 lines
7.5 KiB
Plaintext
224 lines
7.5 KiB
Plaintext
---
|
|
description: Guidelines for managing task dependencies and relationships
|
|
globs: scripts/modules/dependency-manager.js
|
|
alwaysApply: false
|
|
---
|
|
|
|
# Dependency Management Guidelines
|
|
|
|
## Dependency Structure Principles
|
|
|
|
- **Dependency References**:
|
|
- ✅ DO: Represent task dependencies as arrays of task IDs
|
|
- ✅ DO: Use numeric IDs for direct task references
|
|
- ✅ DO: Use string IDs with dot notation (e.g., "1.2") for subtask references
|
|
- ❌ DON'T: Mix reference types without proper conversion
|
|
|
|
```javascript
|
|
// ✅ DO: Use consistent dependency formats
|
|
// For main tasks
|
|
task.dependencies = [1, 2, 3]; // Dependencies on other main tasks
|
|
|
|
// For subtasks
|
|
subtask.dependencies = [1, "3.2"]; // Dependency on main task 1 and subtask 2 of task 3
|
|
```
|
|
|
|
- **Subtask Dependencies**:
|
|
- ✅ DO: Allow numeric subtask IDs to reference other subtasks within the same parent
|
|
- ✅ DO: Convert between formats appropriately when needed
|
|
- ❌ DON'T: Create circular dependencies between subtasks
|
|
|
|
```javascript
|
|
// ✅ DO: Properly normalize subtask dependencies
|
|
// When a subtask refers to another subtask in the same parent
|
|
if (typeof depId === 'number' && depId < 100) {
|
|
// It's likely a reference to another subtask in the same parent task
|
|
const fullSubtaskId = `${parentId}.${depId}`;
|
|
// Now use fullSubtaskId for validation
|
|
}
|
|
```
|
|
|
|
## Dependency Validation
|
|
|
|
- **Existence Checking**:
|
|
- ✅ DO: Validate that referenced tasks exist before adding dependencies
|
|
- ✅ DO: Provide clear error messages for non-existent dependencies
|
|
- ✅ DO: Remove references to non-existent tasks during validation
|
|
|
|
```javascript
|
|
// ✅ DO: Check if the dependency exists before adding
|
|
if (!taskExists(data.tasks, formattedDependencyId)) {
|
|
log('error', `Dependency target ${formattedDependencyId} does not exist in tasks.json`);
|
|
process.exit(1);
|
|
}
|
|
```
|
|
|
|
- **Circular Dependency Prevention**:
|
|
- ✅ DO: Check for circular dependencies before adding new relationships
|
|
- ✅ DO: Use graph traversal algorithms (DFS) to detect cycles
|
|
- ✅ DO: Provide clear error messages explaining the circular chain
|
|
|
|
```javascript
|
|
// ✅ DO: Check for circular dependencies before adding
|
|
const dependencyChain = [formattedTaskId];
|
|
if (isCircularDependency(data.tasks, formattedDependencyId, dependencyChain)) {
|
|
log('error', `Cannot add dependency ${formattedDependencyId} to task ${formattedTaskId} as it would create a circular dependency.`);
|
|
process.exit(1);
|
|
}
|
|
```
|
|
|
|
- **Self-Dependency Prevention**:
|
|
- ✅ DO: Prevent tasks from depending on themselves
|
|
- ✅ DO: Handle both direct and indirect self-dependencies
|
|
|
|
```javascript
|
|
// ✅ DO: Prevent self-dependencies
|
|
if (String(formattedTaskId) === String(formattedDependencyId)) {
|
|
log('error', `Task ${formattedTaskId} cannot depend on itself.`);
|
|
process.exit(1);
|
|
}
|
|
```
|
|
|
|
## Dependency Modification
|
|
|
|
- **Adding Dependencies**:
|
|
- ✅ DO: Format task and dependency IDs consistently
|
|
- ✅ DO: Check for existing dependencies to prevent duplicates
|
|
- ✅ DO: Sort dependencies for better readability
|
|
|
|
```javascript
|
|
// ✅ DO: Format IDs consistently when adding dependencies
|
|
const formattedTaskId = typeof taskId === 'string' && taskId.includes('.')
|
|
? taskId : parseInt(taskId, 10);
|
|
|
|
const formattedDependencyId = formatTaskId(dependencyId);
|
|
```
|
|
|
|
- **Removing Dependencies**:
|
|
- ✅ DO: Check if the dependency exists before removing
|
|
- ✅ DO: Handle different ID formats consistently
|
|
- ✅ DO: Provide feedback about the removal result
|
|
|
|
```javascript
|
|
// ✅ DO: Properly handle dependency removal
|
|
const dependencyIndex = targetTask.dependencies.findIndex(dep => {
|
|
// Convert both to strings for comparison
|
|
let depStr = String(dep);
|
|
|
|
// Handle relative subtask references
|
|
if (typeof dep === 'number' && dep < 100 && isSubtask) {
|
|
const [parentId] = formattedTaskId.split('.');
|
|
depStr = `${parentId}.${dep}`;
|
|
}
|
|
|
|
return depStr === normalizedDependencyId;
|
|
});
|
|
|
|
if (dependencyIndex === -1) {
|
|
log('info', `Task ${formattedTaskId} does not depend on ${formattedDependencyId}, no changes made.`);
|
|
return;
|
|
}
|
|
|
|
// Remove the dependency
|
|
targetTask.dependencies.splice(dependencyIndex, 1);
|
|
```
|
|
|
|
## Dependency Cleanup
|
|
|
|
- **Duplicate Removal**:
|
|
- ✅ DO: Use Set objects to identify and remove duplicates
|
|
- ✅ DO: Handle both numeric and string ID formats
|
|
|
|
```javascript
|
|
// ✅ DO: Remove duplicate dependencies
|
|
const uniqueDeps = new Set();
|
|
const uniqueDependencies = task.dependencies.filter(depId => {
|
|
// Convert to string for comparison to handle both numeric and string IDs
|
|
const depIdStr = String(depId);
|
|
if (uniqueDeps.has(depIdStr)) {
|
|
log('warn', `Removing duplicate dependency from task ${task.id}: ${depId}`);
|
|
return false;
|
|
}
|
|
uniqueDeps.add(depIdStr);
|
|
return true;
|
|
});
|
|
```
|
|
|
|
- **Invalid Reference Cleanup**:
|
|
- ✅ DO: Check for and remove references to non-existent tasks
|
|
- ✅ DO: Check for and remove self-references
|
|
- ✅ DO: Track and report changes made during cleanup
|
|
|
|
```javascript
|
|
// ✅ DO: Filter invalid task dependencies
|
|
task.dependencies = task.dependencies.filter(depId => {
|
|
const numericId = typeof depId === 'string' ? parseInt(depId, 10) : depId;
|
|
if (!validTaskIds.has(numericId)) {
|
|
log('warn', `Removing invalid task dependency from task ${task.id}: ${depId} (task does not exist)`);
|
|
return false;
|
|
}
|
|
return true;
|
|
});
|
|
```
|
|
|
|
## Dependency Visualization
|
|
|
|
- **Status Indicators**:
|
|
- ✅ DO: Use visual indicators to show dependency status (✅/⏱️)
|
|
- ✅ DO: Format dependency lists consistently
|
|
|
|
```javascript
|
|
// ✅ DO: Format dependencies with status indicators
|
|
function formatDependenciesWithStatus(dependencies, allTasks) {
|
|
if (!dependencies || dependencies.length === 0) {
|
|
return 'None';
|
|
}
|
|
|
|
return dependencies.map(depId => {
|
|
const depTask = findTaskById(allTasks, depId);
|
|
if (!depTask) return `${depId} (Not found)`;
|
|
|
|
const isDone = depTask.status === 'done' || depTask.status === 'completed';
|
|
const statusIcon = isDone ? '✅' : '⏱️';
|
|
|
|
return `${statusIcon} ${depId} (${depTask.status})`;
|
|
}).join(', ');
|
|
}
|
|
```
|
|
|
|
## Cycle Detection
|
|
|
|
- **Graph Traversal**:
|
|
- ✅ DO: Use depth-first search (DFS) for cycle detection
|
|
- ✅ DO: Track visited nodes and recursion stack
|
|
- ✅ DO: Support both task and subtask dependencies
|
|
|
|
```javascript
|
|
// ✅ DO: Use proper cycle detection algorithms
|
|
function findCycles(subtaskId, dependencyMap, visited = new Set(), recursionStack = new Set()) {
|
|
// Mark the current node as visited and part of recursion stack
|
|
visited.add(subtaskId);
|
|
recursionStack.add(subtaskId);
|
|
|
|
const cyclesToBreak = [];
|
|
const dependencies = dependencyMap.get(subtaskId) || [];
|
|
|
|
for (const depId of dependencies) {
|
|
if (!visited.has(depId)) {
|
|
const cycles = findCycles(depId, dependencyMap, visited, recursionStack);
|
|
cyclesToBreak.push(...cycles);
|
|
}
|
|
else if (recursionStack.has(depId)) {
|
|
// Found a cycle, add the edge to break
|
|
cyclesToBreak.push(depId);
|
|
}
|
|
}
|
|
|
|
// Remove the node from recursion stack before returning
|
|
recursionStack.delete(subtaskId);
|
|
|
|
return cyclesToBreak;
|
|
}
|
|
```
|
|
|
|
Refer to [`dependency-manager.js`](mdc:scripts/modules/dependency-manager.js) for implementation examples and [`new_features.mdc`](mdc:.cursor/rules/new_features.mdc) for integration guidelines. |