Ralph Khreish 58b417a8ce
Add complexity score to task (#528)
* feat: added complexity score handling to list tasks

* feat: added handling for complexity score in find task by id

* test: remove console dir

* chore: add changeset

* format: fixed formatting issues

* ref: reorder imports

* feat: updated handling for findTaskById to take complexityReport as input

* test: fix findTaskById complexity report testcases

* fix: added handling for complexity report path

* chore: add changeset

* fix: moved complexity report handling to list tasks rather than list tasks direct

* fix: add complexity handling to next task in list command

* fix: added handling for show cli

* fix: fixed next cli command handling

* fix: fixed handling for complexity report path in mcp

* feat: added handling to get-task

* feat: added handling for next-task in mcp

* feat: add handling for report path override

* chore: remove unecessary changeset

* ref: remove unecessary comments

* feat: update list and find next task

* fix: fixed running tests

* fix: fixed findTaskById

* fix: fixed findTaskById and tests

* fix: fixed addComplexityToTask util

* fix: fixed mcp server project root input

* chore: cleanup

---------

Co-authored-by: Shrey Paharia <shreypaharia@gmail.com>
2025-05-16 23:24:25 +02:00

139 lines
4.5 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { log } from '../utils.js';
import { addComplexityToTask } from '../utils.js';
/**
* Return the next work item:
* • Prefer an eligible SUBTASK that belongs to any parent task
* whose own status is `in-progress`.
* • If no such subtask exists, fall back to the best top-level task
* (previous behaviour).
*
* The function still exports the same name (`findNextTask`) so callers
* don't need to change. It now always returns an object with
* ─ id → number (task) or "parentId.subId" (subtask)
* ─ title → string
* ─ status → string
* ─ priority → string ("high" | "medium" | "low")
* ─ dependencies → array (all IDs expressed in the same dotted form)
* ─ parentId → number (present only when it's a subtask)
*
* @param {Object[]} tasks full array of top-level tasks, each may contain .subtasks[]
* @param {Object} [complexityReport=null] - Optional complexity report object
* @returns {Object|null} next work item or null if nothing is eligible
*/
function findNextTask(tasks, complexityReport = null) {
// ---------- helpers ----------------------------------------------------
const priorityValues = { high: 3, medium: 2, low: 1 };
const toFullSubId = (parentId, maybeDotId) => {
// "12.3" -> "12.3"
// 4 -> "12.4" (numeric / short form)
if (typeof maybeDotId === 'string' && maybeDotId.includes('.')) {
return maybeDotId;
}
return `${parentId}.${maybeDotId}`;
};
// ---------- build completed-ID set (tasks *and* subtasks) --------------
const completedIds = new Set();
tasks.forEach((t) => {
if (t.status === 'done' || t.status === 'completed') {
completedIds.add(String(t.id));
}
if (Array.isArray(t.subtasks)) {
t.subtasks.forEach((st) => {
if (st.status === 'done' || st.status === 'completed') {
completedIds.add(`${t.id}.${st.id}`);
}
});
}
});
// ---------- 1) look for eligible subtasks ------------------------------
const candidateSubtasks = [];
tasks
.filter((t) => t.status === 'in-progress' && Array.isArray(t.subtasks))
.forEach((parent) => {
parent.subtasks.forEach((st) => {
const stStatus = (st.status || 'pending').toLowerCase();
if (stStatus !== 'pending' && stStatus !== 'in-progress') return;
const fullDeps =
st.dependencies?.map((d) => toFullSubId(parent.id, d)) ?? [];
const depsSatisfied =
fullDeps.length === 0 ||
fullDeps.every((depId) => completedIds.has(String(depId)));
if (depsSatisfied) {
candidateSubtasks.push({
id: `${parent.id}.${st.id}`,
title: st.title || `Subtask ${st.id}`,
status: st.status || 'pending',
priority: st.priority || parent.priority || 'medium',
dependencies: fullDeps,
parentId: parent.id
});
}
});
});
if (candidateSubtasks.length > 0) {
// sort by priority → dep-count → parent-id → sub-id
candidateSubtasks.sort((a, b) => {
const pa = priorityValues[a.priority] ?? 2;
const pb = priorityValues[b.priority] ?? 2;
if (pb !== pa) return pb - pa;
if (a.dependencies.length !== b.dependencies.length)
return a.dependencies.length - b.dependencies.length;
// compare parent then sub-id numerically
const [aPar, aSub] = a.id.split('.').map(Number);
const [bPar, bSub] = b.id.split('.').map(Number);
if (aPar !== bPar) return aPar - bPar;
return aSub - bSub;
});
const nextTask = candidateSubtasks[0];
// Add complexity to the task before returning
if (nextTask && complexityReport) {
addComplexityToTask(nextTask, complexityReport);
}
return nextTask;
}
// ---------- 2) fall back to top-level tasks (original logic) ------------
const eligibleTasks = tasks.filter((task) => {
const status = (task.status || 'pending').toLowerCase();
if (status !== 'pending' && status !== 'in-progress') return false;
const deps = task.dependencies ?? [];
return deps.every((depId) => completedIds.has(String(depId)));
});
if (eligibleTasks.length === 0) return null;
const nextTask = eligibleTasks.sort((a, b) => {
const pa = priorityValues[a.priority || 'medium'] ?? 2;
const pb = priorityValues[b.priority || 'medium'] ?? 2;
if (pb !== pa) return pb - pa;
const da = (a.dependencies ?? []).length;
const db = (b.dependencies ?? []).length;
if (da !== db) return da - db;
return a.id - b.id;
})[0];
// Add complexity to the task before returning
if (nextTask && complexityReport) {
addComplexityToTask(nextTask, complexityReport);
}
return nextTask;
}
export default findNextTask;