/** * Task hierarchy operations - parent/child tree traversal and validation. * Ported from lib/tasks/hierarchy.sh * * @epic T4454 * @task T4529 */ import type { Task, TaskType } from '@cleocode/contracts'; /** * Get direct children of a task. */ export declare function getChildren(taskId: string, tasks: Task[]): Task[]; /** * Get direct child IDs. */ export declare function getChildIds(taskId: string, tasks: Task[]): string[]; /** * Get all descendants of a task (recursive). */ export declare function getDescendants(taskId: string, tasks: Task[]): Task[]; /** * Get all descendant IDs (flat list). */ export declare function getDescendantIds(taskId: string, tasks: Task[]): string[]; /** * Get the parent chain (ancestors) from a task up to the root. * Returns ordered from immediate parent to root. */ export declare function getParentChain(taskId: string, tasks: Task[]): Task[]; /** * Get the parent chain as IDs. */ export declare function getParentChainIds(taskId: string, tasks: Task[]): string[]; /** * Calculate depth of a task in the hierarchy (0-based). * Root tasks have depth 0, their children depth 1, etc. */ export declare function getDepth(taskId: string, tasks: Task[]): number; /** * Whether adding a child under a parent at `parentDepth` would exceed the cap. * * `maxDepth` is the maximum depth VALUE a node may hold, INCLUSIVE — not a count * of tiers. The canonical spine is `saga(0) → epic(1) → task(2) → subtask(3)` * (see the `TaskType` TSDoc in `@cleocode/contracts`), so the default * `maxDepth: 3` admits a subtask and refuses anything below it. * * The inclusive reading is the one the config schema, `validateParent`, and * `compliance/protocol-rules.ts` have always used. The four write-path guards * that now delegate here used `parentDepth + 1 >= maxDepth`, an EXCLUSIVE * reading that was correct only while the spine was three tiers (`epic=0, * task=1, subtask=2` — still the wording in `core/schemas/config.schema.json`). * ADR-083 §2.5 / ADR-088 made `saga` a real `parent_id` container, pushing every * tier down one, but the comparison was never recalibrated. The saga silently * consumed the level the subtask needed, so `cleo add --type subtask` became * unreachable in every saga-rooted project. Measured on this repo's own store: * 163 live `saga → epic → task → subtask` rows the add path could no longer * create, plus two field reports of agents thrashing on `E_CLEO_DEPTH_EXCEEDED`. * * Kept as ONE function on purpose. The comparison previously existed in four * copies (`tasks/add.ts`, `tasks/update.ts`, `validateHierarchyPlacement`, and * `validateHierarchy` below) and drifted away from the two correct * implementations without any of them failing a test — every depth test asserted * the arithmetic over untyped fixtures, so inserting a tier above `epic` could * not fail them. The next tier change edits this line. * * @param parentDepth - Ancestor count of the prospective parent (root = 0). * @param maxDepth - Resolved `HierarchyPolicy.maxDepth` cap, inclusive. * @returns `true` when the child would sit deeper than `maxDepth`. * * @example * ```ts * // saga(0) → epic(1) → task(2): adding a subtask under the task * console.assert(exceedsMaxDepth(2, 3) === false, 'subtask at depth 3 is legal'); * // adding under that subtask would be depth 4 * console.assert(exceedsMaxDepth(3, 3) === true, 'depth 4 is refused'); * ``` */ export declare function exceedsMaxDepth(parentDepth: number, maxDepth: number): boolean; /** * Derive the tier a child takes under a given parent, per the PM-Core V2 * containment matrix (`saga→epic`, `epic→task`, `task→subtask`). * * `currentType` pins the tier for nodes whose type is intrinsic rather than * positional: a saga stays a saga and an epic stays an epic wherever it is * moved. It is optional so the add path — which has no existing row — can ask * the same question as the reparent paths. * * Canonicalised here because three callers had byte-identical private copies * (`tasks/update.ts`, `tasks/task-reparent.ts`, and the inline expression in * `tasks/add.ts`), and the add path needed a fourth to cross-check its own error * suggestions. * * The `saga` parent branch is deliberately CREATE-ONLY (`currentType === * undefined`). On the add path a typeless child of a saga is an epic. On a * reparent the node already has a tier, and returning `epic` there would * silently PROMOTE a task to an epic to satisfy the containment matrix instead * of rejecting the move — so an existing node falls through and is refused by * `isAllowedWorkGraphParentType`, exactly as the private copies did. * * @param parentType - Type of the prospective parent, or null/undefined for root. * @param currentType - Existing type of the node being placed, when it has one. * @returns The tier the child should hold under that parent. */ export declare function childTypeForParentType(parentType: TaskType | null | undefined, currentType?: TaskType): TaskType; /** * Get the root ancestor of a task. */ export declare function getRootAncestor(taskId: string, tasks: Task[]): Task | null; /** * Check if a task is an ancestor of another. */ export declare function isAncestorOf(ancestorId: string, descendantId: string, tasks: Task[]): boolean; /** * Check if a task is a descendant of another. */ export declare function isDescendantOf(descendantId: string, ancestorId: string, tasks: Task[]): boolean; /** * Get sibling tasks (same parent). */ export declare function getSiblings(taskId: string, tasks: Task[]): Task[]; /** * Validate that adding a child to a parent would not violate constraints. */ export interface HierarchyValidation { valid: boolean; error?: { code: string; message: string; }; } export declare function validateHierarchy(parentId: string | null, tasks: Task[], policy?: { maxDepth?: number; maxSiblings?: number; }): HierarchyValidation; /** * Detect circular reference if parentId were set. */ export declare function wouldCreateCircle(taskId: string, newParentId: string, tasks: Task[]): boolean; /** * Build a tree structure from flat task list. */ export interface TaskTreeNode { task: Task; children: TaskTreeNode[]; } export declare function buildTree(tasks: Task[]): TaskTreeNode[]; /** * Flatten a tree back to a list (depth-first). */ export declare function flattenTree(nodes: TaskTreeNode[]): Task[]; //# sourceMappingURL=hierarchy.d.ts.map