import { g as getTasksDb } from './connections2-D22b0xbS.js';

//#region src/lib/server/tasks/explorer-loader.ts
/**
* Parse a JSON column into a string array, returning `[]` on any failure.
*
* Used for `labels_json`, `notes_json`, `files_json`. Defensive: bad JSON
* must not poison the entire bundle — silently degrade to empty.
*/
function parseStringArray(raw) {
	if (!raw) return [];
	try {
		const parsed = JSON.parse(raw);
		if (!Array.isArray(parsed)) return [];
		return parsed.filter((x) => typeof x === "string");
	} catch {
		return [];
	}
}
/**
* Parse a JSON column into an unknown array, preserving object entries.
*
* Used for `acceptance_json` (mixed strings + AcceptanceGate objects) and
* for passing through `verification_json` shape without imposing strict
* validation in this loader.
*/
function parseJsonArray(raw) {
	if (!raw) return [];
	try {
		const parsed = JSON.parse(raw);
		return Array.isArray(parsed) ? parsed : [];
	} catch {
		return [];
	}
}
/**
* Parse a JSON column into an object, returning `undefined` on any failure.
*/
function parseJsonObject(raw) {
	if (!raw) return void 0;
	try {
		const parsed = JSON.parse(raw);
		return typeof parsed === "object" && parsed !== null ? parsed : void 0;
	} catch {
		return;
	}
}
/**
* Convert a raw snake_case SQLite row to the canonical camelCase
* {@link Task} contract used across CLEO.
*
* Mirrors `packages/core/src/store/converters.ts#rowToTask` for consumers
* that read directly from `node:sqlite` (Studio server load) instead of the
* Drizzle-backed DataAccessor path.
*/
function rowToTask(row) {
	const provenance = row.created_by !== null || row.modified_by !== null || row.session_id !== null ? {
		createdBy: row.created_by,
		modifiedBy: row.modified_by,
		sessionId: row.session_id
	} : void 0;
	return {
		id: row.id,
		title: row.title,
		description: row.description ?? "",
		status: row.status,
		priority: row.priority,
		type: row.type ?? void 0,
		parentId: row.parent_id ?? void 0,
		position: row.position ?? void 0,
		positionVersion: row.position_version ?? void 0,
		size: row.size ?? void 0,
		phase: row.phase ?? void 0,
		files: parseStringArray(row.files_json),
		acceptance: parseJsonArray(row.acceptance_json),
		labels: parseStringArray(row.labels_json),
		notes: parseStringArray(row.notes_json),
		origin: row.origin ?? void 0,
		blockedBy: row.blocked_by ?? void 0,
		epicLifecycle: row.epic_lifecycle ?? void 0,
		noAutoComplete: row.no_auto_complete === null ? void 0 : row.no_auto_complete !== 0,
		createdAt: row.created_at,
		updatedAt: row.updated_at ?? void 0,
		completedAt: row.completed_at ?? void 0,
		cancelledAt: row.cancelled_at ?? void 0,
		cancellationReason: row.cancellation_reason ?? void 0,
		verification: parseJsonObject(row.verification_json),
		provenance,
		pipelineStage: row.pipeline_stage ?? void 0,
		assignee: row.assignee ?? void 0
	};
}
/**
* Build `{ epicId -> EpicProgressBucket }` from the already-loaded task
* array.
*
* Pure function — no DB access. Algorithm:
*
* 1. Bucket every task by `parentId` into `childrenByParent`.
* 2. For every epic (`type = 'epic'`), look up its direct children and tally
*    their statuses.
*
* Complexity: O(N) where N is `tasks.length`.
*
* @remarks
* Archived children are already filtered out at the SQL layer when
* `includeArchived` is false. When `includeArchived` is true, archived
* children are included in `total` but NOT tallied in any of
* `done/cancelled/active` (they belong in none of those buckets).
*/
function _computeEpicProgressRollup(tasks) {
	const childrenByParent = /* @__PURE__ */ new Map();
	for (const task of tasks) {
		const parent = task.parentId;
		if (!parent) continue;
		const bucket = childrenByParent.get(parent);
		if (bucket) bucket.push(task);
		else childrenByParent.set(parent, [task]);
	}
	const result = {};
	for (const task of tasks) {
		if (task.type !== "epic") continue;
		const children = childrenByParent.get(task.id) ?? [];
		let done = 0;
		let cancelled = 0;
		let active = 0;
		for (const child of children) switch (child.status) {
			case "done":
				done += 1;
				break;
			case "cancelled":
				cancelled += 1;
				break;
			case "active":
				active += 1;
				break;
		}
		result[task.id] = {
			total: children.length,
			done,
			cancelled,
			active
		};
	}
	return result;
}
/**
* Aggregate distinct labels from the loaded task set. Output is sorted for
* deterministic rendering in the `FilterChipGroup`.
*/
function _collectDistinctLabels(tasks) {
	const seen = /* @__PURE__ */ new Set();
	for (const task of tasks) {
		const labels = task.labels;
		if (!labels) continue;
		for (const label of labels) if (typeof label === "string" && label.length > 0) seen.add(label);
	}
	return [...seen].sort();
}
/** SELECT list that maps 1:1 to {@link TaskRowSqlite}. Hoisted for reuse. */
var TASKS_SELECT_COLUMNS = `
  id, title, description, status, priority, type, parent_id, pipeline_stage,
  size, phase, position, position_version, labels_json, acceptance_json,
  files_json, notes_json, verification_json, origin, blocked_by,
  epic_lifecycle, no_auto_complete, created_at, updated_at, completed_at,
  cancelled_at, cancellation_reason, created_by, modified_by, session_id,
  assignee
`;
/** Fallback empty bundle returned when tasks.db does not exist. */
function emptyBundle() {
	return {
		tasks: [],
		deps: [],
		epicProgress: {},
		labels: [],
		loadedAt: (/* @__PURE__ */ new Date()).toISOString()
	};
}
/**
* Load the full Task Explorer payload for the active project context.
*
* @param opts - Load options (project context + optional filters).
* @returns A snapshot of the entire explorer-relevant data set.
*
* @remarks
* When `tasks.db` does not exist for the project (e.g. freshly-registered
* project that hasn't run `cleo init`), returns an empty bundle rather than
* throwing. Callers render a "tasks.db not found" empty state.
*
* Closes the SQLite connection before returning so the loader does not leak
* file handles. One connection per call is fine — `node:sqlite` open is
* sub-millisecond per {@link getTasksDb} documentation.
*
* @example
* ```ts
* // In packages/studio/src/routes/tasks/+page.server.ts
* export const load: PageServerLoad = async ({ locals, url }) => {
*   const bundle = await loadExplorerBundle({ projectCtx: locals.projectCtx });
*   return { bundle };
* };
* ```
*/
async function loadExplorerBundle(opts) {
	const { projectCtx } = opts;
	const includeArchived = opts.includeArchived ?? false;
	const rawLimit = opts.limit ?? 2e3;
	const limit = Number.isFinite(rawLimit) && rawLimit > 0 ? Math.floor(rawLimit) : 2e3;
	const db = getTasksDb(projectCtx);
	if (!db) return emptyBundle();
	try {
		const whereClause = includeArchived ? "" : `WHERE status != 'archived'`;
		const tasks = db.prepare(`SELECT ${TASKS_SELECT_COLUMNS}
         FROM tasks
         ${whereClause}
         ORDER BY id ASC
         LIMIT ?`).all(limit).map(rowToTask);
		const depRows = db.prepare("SELECT task_id, depends_on FROM task_dependencies").all();
		const loadedIds = new Set(tasks.map((t) => t.id));
		return {
			tasks,
			deps: depRows.filter((r) => loadedIds.has(r.task_id) && loadedIds.has(r.depends_on)).map((r) => ({
				taskId: r.task_id,
				dependsOn: r.depends_on
			})),
			epicProgress: _computeEpicProgressRollup(tasks),
			labels: _collectDistinctLabels(tasks),
			loadedAt: (/* @__PURE__ */ new Date()).toISOString()
		};
	} finally {
		db.close();
	}
}

export { loadExplorerBundle as l };
//# sourceMappingURL=explorer-loader-B_Iv2bTD.js.map
