diff --git a/core/models.py b/core/models.py index b21b1af..48077d2 100644 --- a/core/models.py +++ b/core/models.py @@ -274,19 +274,29 @@ def get_children(conn: sqlite3.Connection, task_id: str) -> list[dict]: return _rows_to_list(rows) -def has_open_children(conn: sqlite3.Connection, task_id: str) -> bool: +def has_open_children(conn: sqlite3.Connection, task_id: str, visited: set[str] | None = None) -> bool: """Recursively check if task has any open (not done/cancelled) descendants.""" + if visited is None: + visited = set() + if task_id in visited: + return False + visited = visited | {task_id} children = get_children(conn, task_id) for child in children: if child["status"] not in ("done", "cancelled"): return True - if has_open_children(conn, child["id"]): + if has_open_children(conn, child["id"], visited): return True return False -def _check_parent_completion(conn: sqlite3.Connection, task_id: str) -> None: +def _check_parent_completion(conn: sqlite3.Connection, task_id: str, visited: set[str] | None = None) -> None: """Cascade-check upward: if parent is 'revising' and all children closed → promote to 'done'.""" + if visited is None: + visited = set() + if task_id in visited: + return + visited = visited | {task_id} task = get_task(conn, task_id) if not task: return @@ -305,7 +315,7 @@ def _check_parent_completion(conn: sqlite3.Connection, task_id: str) -> None: (now, now, parent_id), ) conn.commit() - _check_parent_completion(conn, parent_id) + _check_parent_completion(conn, parent_id, visited) VALID_TASK_SORT_FIELDS = frozenset({