123 lines
4.0 KiB
JavaScript
Raw 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.

/**
* 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[]
* @returns {Object|null} next work item or null if nothing is eligible
*/
function findNextTask(tasks) {
// ---------- 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;
});
return candidateSubtasks[0];
}
// ---------- 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];
return nextTask;
}
export default findNextTask;