import { b as getNativeTasksDb } from './sqlite-Dsbe0wec.js';

//#region ../core/dist/lifecycle/rollup.js
/**
* Task rollup — canonical view of a task's execution + pipeline + children state.
*
* `computeTaskRollup` is the SINGLE source of truth consumed by Studio, the
* SDK, and the CLI for answering "what is the state of this task?" Prior to
* T943 each surface independently stitched together `tasks.status`,
* `tasks.pipelineStage`, child counts, and gate results, producing divergent
* views (e.g. `/tasks` vs `/tasks/pipeline` disagreeing about epic progress).
*
* Design contract:
*   - Pure read — no writes, no side effects.
*   - `pipelineStage` reads `tasks.pipelineStage` directly until T947 migrates
*     it to the derived lifecycle view.
*   - Archived children are EXCLUDED from both `childrenTotal` and
*     `childrenDone` so epic-progress bars reflect in-flight scope.
*   - `gatesVerified` is populated only when `lifecycle_gate_results` has at
*     least one `result='pass'` row linked to a stage owned by this task's
*     pipeline. Missing rows → empty array.
*   - `blockedBy` parses `tasks.blocked_by` robustly: JSON array, falling back
*     to comma-separated tokens, falling back to the raw string, and
*     collapsing to `[]` on empty/null.
*   - Batch API (`computeTaskRollups`) preserves input order and returns the
*     entries for missing task IDs as `null`-free by filtering them out. The
*     caller can re-align using the returned `id`s.
*
* @task T943
*/
/**
* Parse the `tasks.blocked_by` text column into a canonical string array.
*
* Strategy (first match wins):
*   1. Non-empty JSON array of strings → use as-is after trimming.
*   2. Comma-separated list → split + trim + drop empties.
*   3. Single non-empty token → wrap in a one-element array.
*   4. Null / empty / whitespace-only → empty array.
*
* Never throws; silently falls back to the next strategy on parse error.
*/
function parseBlockedBy(raw) {
	if (raw === null || raw === void 0) return [];
	const trimmed = raw.trim();
	if (trimmed.length === 0) return [];
	if (trimmed.startsWith("[")) try {
		const parsed = JSON.parse(trimmed);
		if (Array.isArray(parsed)) return parsed.filter((v) => typeof v === "string").map((v) => v.trim()).filter((v) => v.length > 0);
	} catch {}
	if (trimmed.includes(",")) return trimmed.split(",").map((v) => v.trim()).filter((v) => v.length > 0);
	return [trimmed];
}
/**
* Compute the `lastActivityAt` timestamp.
*
* Preference order: `completedAt` vs `updatedAt`, whichever is lexicographically
* greater (ISO-8601 strings sort correctly when both use the same zone).
* Returns `null` when neither is set.
*/
function computeLastActivityAt(updatedAt, completedAt) {
	const u = updatedAt ?? null;
	const c = completedAt ?? null;
	if (u === null && c === null) return null;
	if (u === null) return c;
	if (c === null) return u;
	return u >= c ? u : c;
}
/**
* Normalise a raw `tasks.status` enum value into a {@link RollupExecStatus}.
*
* Every value the DB CHECK constraint allows is already a member of the
* rollup union, so this is a pure widening cast — but we keep it centralised
* so the T947 migration (`proposed` from external intake) has one place to
* hook into.
*/
function toRollupExecStatus(status) {
	return status;
}
/**
* Aggregate non-archived children per parent in a single query.
*
* Returns a map keyed by parent_id with `{ total, done }` counts. Parents with
* zero non-archived children are omitted so the caller defaults to 0.
*/
function fetchChildAggregates(taskIds) {
	const empty = /* @__PURE__ */ new Map();
	if (taskIds.length === 0) return empty;
	const native = getNativeTasksDb();
	if (!native) return empty;
	const sqlText = `
    SELECT
      parent_id           AS parent_id,
      COUNT(*)            AS children_total,
      SUM(CASE WHEN status = 'done' THEN 1 ELSE 0 END) AS children_done
    FROM tasks_tasks
    WHERE parent_id IN (${taskIds.map(() => "?").join(", ")})
      AND status != 'archived'
    GROUP BY parent_id
  `;
	const rows = native.prepare(sqlText).all(...taskIds);
	const map = /* @__PURE__ */ new Map();
	for (const row of rows) {
		if (row.parent_id === null) continue;
		map.set(row.parent_id, {
			total: Number(row.children_total ?? 0),
			done: Number(row.children_done ?? 0)
		});
	}
	return map;
}
/**
* Fetch gate names with `result='pass'` for every task in a single query.
*
* Joins `lifecycle_gate_results` → `lifecycle_stages` → `lifecycle_pipelines`
* and filters by `pipelines.task_id`. Returns `[]` per-task when the task has
* no pipeline or when every gate result is non-pass.
*/
function fetchPassedGateNames(taskIds) {
	const empty = /* @__PURE__ */ new Map();
	if (taskIds.length === 0) return empty;
	const native = getNativeTasksDb();
	if (!native) return empty;
	const sqlText = `
    SELECT DISTINCT
      p.task_id   AS task_id,
      g.gate_name AS gate_name
    FROM tasks_lifecycle_gate_results g
    INNER JOIN tasks_lifecycle_stages s     ON s.id = g.stage_id
    INNER JOIN tasks_lifecycle_pipelines p  ON p.id = s.pipeline_id
    WHERE p.task_id IN (${taskIds.map(() => "?").join(", ")})
      AND g.result = 'pass'
    ORDER BY g.gate_name ASC
  `;
	let rows;
	try {
		rows = native.prepare(sqlText).all(...taskIds);
	} catch {
		return empty;
	}
	const map = /* @__PURE__ */ new Map();
	for (const row of rows) {
		if (row.task_id === null || row.gate_name === null) continue;
		const list = map.get(row.task_id);
		if (list === void 0) map.set(row.task_id, [row.gate_name]);
		else if (!list.includes(row.gate_name)) list.push(row.gate_name);
	}
	return map;
}
/**
* Assemble a {@link TaskRollup} from its raw inputs.
*/
function buildRollup(task, childAggregate, gatesVerified) {
	return {
		id: task.id,
		execStatus: toRollupExecStatus(task.status),
		pipelineStage: task.pipelineStage ?? null,
		gatesVerified,
		childrenTotal: childAggregate?.total ?? 0,
		childrenDone: childAggregate?.done ?? 0,
		blockedBy: parseBlockedBy(task.blockedBy),
		lastActivityAt: computeLastActivityAt(task.updatedAt, task.completedAt)
	};
}
/**
* Compute rollups for many tasks in a single batch.
*
* Issues exactly one task load, one child-aggregate query, and one gate query
* regardless of batch size. Results preserve the input order of `taskIds`.
* Missing IDs are omitted from the return array — the caller can detect them
* by comparing `taskIds.length` vs `result.length` or by mapping on `id`.
*
* @param taskIds - Ordered list of task identifiers to roll up.
* @param dataAccessor - Storage accessor used for the batched task load.
* @returns Rollups in the same order as `taskIds`, minus any missing ids.
*/
async function computeTaskRollups(taskIds, dataAccessor) {
	if (taskIds.length === 0) return [];
	const loaded = await dataAccessor.loadTasks(taskIds);
	const byId = /* @__PURE__ */ new Map();
	for (const task of loaded) byId.set(task.id, task);
	const presentIds = taskIds.filter((id) => byId.has(id));
	const childMap = fetchChildAggregates(presentIds);
	const gateMap = fetchPassedGateNames(presentIds);
	const rollups = [];
	for (const id of taskIds) {
		const task = byId.get(id);
		if (task === void 0) continue;
		rollups.push(buildRollup(task, childMap.get(id), gateMap.get(id) ?? []));
	}
	return rollups;
}

export { computeTaskRollups as c };
//# sourceMappingURL=rollup-DNaP6j6l.js.map
