// SF Extension — DB-backed State Derivation
// All private helpers and the exported deriveStateFromDb() that queries
// the SQLite milestone/slice/task tables directly.

import { randomUUID } from "node:crypto";
import { existsSync, readdirSync } from "node:fs";
import { join } from "node:path";
import {
	isValidTaskSummary,
	loadFile,
	parseRequirementCounts,
	parseRequirementsByMilestone,
	parseSummary,
} from "./files.js";
import { findMilestoneIds } from "./milestone-ids.js";
import {
	resolveMilestoneFile,
	resolveSfRootFile,
	resolveSliceFile,
	resolveSlicePath,
	resolveTaskFile,
	resolveTasksDir,
} from "./paths.js";
import { loadQueueOrder, sortByQueueOrder } from "./queue-order.js";
import {
	getActiveRequirements,
	getAllMilestones,
	getMilestone,
	getMilestoneSlices,
	getMilestoneValidationAssessment,
	getPendingGateCountForTurn,
	getReplanHistory,
	getSlice,
	getSliceTasks,
	insertSelfFeedbackEntry,
	isDbAvailable,
} from "./sf-db.js";
import {
	canonicalMilestonePrefix,
	extractContextTitle,
	isGhostMilestone,
	isMilestoneComplete,
	loadTerminalSummary,
	readMilestoneValidationVerdict,
	stripMilestonePrefix,
} from "./state-shared.js";
import { isClosedStatus, isInactiveStatus } from "./status-guards.js";
import { logWarning } from "./workflow-logger.js";

// ─── DB-backed State Derivation ────────────────────────────────────────────
// isStatusDone replaced by isClosedStatus from status-guards.ts (single source of truth).
// Alias kept for backward compatibility within this file.
const isStatusDone = isClosedStatus;

function milestoneIdFromRequirementOwner(owner) {
	const match = String(owner ?? "").match(/^(M\d+)(?:\/S\d+)?$/);
	return match?.[1] ?? null;
}

function addRequirementMilestoneCount(result, owner, status) {
	const milestoneId = milestoneIdFromRequirementOwner(owner);
	if (!milestoneId) return;
	const current = result.get(milestoneId) ?? { complete: 0, incomplete: 0 };
	if (isClosedStatus(String(status ?? "").toLowerCase())) current.complete += 1;
	else current.incomplete += 1;
	result.set(milestoneId, current);
}

/**
 * Return requirement completion counts keyed by owning milestone.
 *
 * Purpose: let the milestone state machine honor the DB-backed requirements
 * contract before falling back to slice artifacts or markdown projections.
 *
 * Consumer: handleAllSlicesDone when deciding whether a milestone with only
 * skipped/stale slices should complete or re-enter pre-planning.
 */
async function readRequirementsByMilestone(basePath) {
	if (isDbAvailable()) {
		const result = new Map();
		for (const req of getActiveRequirements()) {
			addRequirementMilestoneCount(result, req.primary_owner, req.status);
		}
		if (result.size > 0) return result;
	}
	const reqsFile = resolveSfRootFile(basePath, "REQUIREMENTS");
	const reqsContent = reqsFile ? await loadFile(reqsFile) : null;
	return parseRequirementsByMilestone(reqsContent ?? "");
}

/**
 * Derive SF state from the milestones/slices/tasks DB tables.
 * Non-planning control files (PARKED, CONTINUE, REPLAN, REPLAN-TRIGGER,
 * CONTEXT-DRAFT) are still checked on the filesystem since they are not
 * hierarchy state.
 * Requirements also stay file-based via parseRequirementCounts().
 *
 * Must not import rendered roadmap, plan, or summary artifacts into DB-backed
 * runtime state. Explicit migration/repair flows own any legacy file import.
 */
function reconcileDiskToDb(basePath) {
	const diskIds = findMilestoneIds(basePath);
	if (diskIds.length > 0) {
		const dbIds = new Set(getAllMilestones().map((m) => m.id));
		const dbPrefixes = new Set(
			Array.from(dbIds, (id) => canonicalMilestonePrefix(id)),
		);
		const diskOnlyIds = diskIds.filter(
			(id) =>
				!dbIds.has(id) &&
				!dbPrefixes.has(canonicalMilestonePrefix(id)) &&
				!isGhostMilestone(basePath, id),
		);
		if (diskOnlyIds.length > 0) {
			logWarning(
				"state",
				`DB-backed state ignored ${diskOnlyIds.length} disk-only milestone(s): ${diskOnlyIds.join(", ")}`,
			);
		}
	}
	return getAllMilestones();
}
function buildCompletenessSet(basePath, milestones) {
	const completeMilestoneIds = new Set();
	const parkedMilestoneIds = new Set();
	// DB-authoritative: a milestone is only "complete" when its DB row says so.
	// SUMMARY-file presence is NOT a completion signal here — an orphan SUMMARY
	// (crashed complete-milestone turn, partial merge, manual edit) must not
	// flip derived state to complete and cascade into a false auto-merge (#4179).
	for (const m of milestones) {
		const parkedFile = resolveMilestoneFile(basePath, m.id, "PARKED");
		if (parkedFile || m.status === "parked") {
			parkedMilestoneIds.add(m.id);
			continue;
		}
		if (isStatusDone(m.status)) {
			completeMilestoneIds.add(m.id);
		}
	}
	return { completeMilestoneIds, parkedMilestoneIds };
}
async function buildRegistryAndFindActive(
	basePath,
	milestones,
	completeMilestoneIds,
	parkedMilestoneIds,
) {
	const registry = [];
	let activeMilestone = null;
	let activeMilestoneSlices = [];
	let activeMilestoneFound = false;
	let activeMilestoneHasDraft = false;
	let firstDeferredQueuedShell = null;
	for (const m of milestones) {
		if (parkedMilestoneIds.has(m.id)) {
			registry.push({
				id: m.id,
				title: stripMilestonePrefix(m.title) || m.id,
				status: "parked",
			});
			continue;
		}
		// #sf-mp8aotmq-jxby91: cancelled milestones are user-dropped, never to be
		// picked as activeMilestone. isClosedStatus intentionally excludes
		// "cancelled" (status-guards.js comment) because cancelled !== complete,
		// but for active-selection both must be skipped. Without this branch the
		// dispatcher routes plan-milestone at cancelled stubs.
		if (m.status === "cancelled") {
			registry.push({
				id: m.id,
				title: stripMilestonePrefix(m.title) || m.id,
				status: "cancelled",
			});
			continue;
		}
		const slices = getMilestoneSlices(m.id);
		if (
			slices.length === 0 &&
			!isStatusDone(m.status) &&
			m.status !== "queued"
		) {
			if (isGhostMilestone(basePath, m.id)) continue;
		}
		// DB-authoritative completeness (#4179): only trust completeMilestoneIds,
		// which is itself derived from DB status. SUMMARY-file presence alone must
		// not imply completion. The summary file may still be consulted below as a
		// title source for legitimately-complete milestones whose DB row has no title.
		if (completeMilestoneIds.has(m.id)) {
			let title = stripMilestonePrefix(m.title) || m.id;
			if (!m.title) {
				const summaryFile = resolveMilestoneFile(basePath, m.id, "SUMMARY");
				if (summaryFile) {
					const summaryContent = await loadFile(summaryFile);
					if (summaryContent) {
						title = parseSummary(summaryContent).title || m.id;
					}
				}
			}
			registry.push({ id: m.id, title, status: "complete" });
			continue;
		}
		const allSlicesDone =
			slices.length > 0 && slices.every((s) => isStatusDone(s.status));
		let title = stripMilestonePrefix(m.title) || m.id;
		if (title === m.id) {
			const contextFile = resolveMilestoneFile(basePath, m.id, "CONTEXT");
			const draftFile = resolveMilestoneFile(basePath, m.id, "CONTEXT-DRAFT");
			const contextContent = contextFile ? await loadFile(contextFile) : null;
			const draftContent =
				draftFile && !contextContent ? await loadFile(draftFile) : null;
			title = extractContextTitle(contextContent || draftContent, m.id);
		}
		if (!activeMilestoneFound) {
			const deps = m.depends_on;
			const depsUnmet = deps.some((dep) => !completeMilestoneIds.has(dep));
			if (depsUnmet) {
				registry.push({ id: m.id, title, status: "pending", dependsOn: deps });
				continue;
			}
			if (m.status === "queued" && slices.length === 0) {
				const contextFile = resolveMilestoneFile(basePath, m.id, "CONTEXT");
				const draftFile = resolveMilestoneFile(basePath, m.id, "CONTEXT-DRAFT");
				if (!contextFile && !draftFile) {
					if (!firstDeferredQueuedShell) {
						firstDeferredQueuedShell = { id: m.id, title, deps };
					}
					registry.push({
						id: m.id,
						title,
						status: "pending",
						...(deps.length > 0 ? { dependsOn: deps } : {}),
					});
					continue;
				}
			}
			if (allSlicesDone) {
				const { terminal: validationTerminal } =
					await readMilestoneValidationVerdict(basePath, m.id, loadFile);
				// DB-authoritative (#4179): completeness is already decided by
				// completeMilestoneIds above. If we reached this branch, the DB says
				// the milestone is NOT complete — so any SUMMARY file on disk is an
				// orphan (crashed complete-milestone, partial merge, manual edit) and
				// must not short-circuit this path. When validation is terminal, fall
				// through to the default active-push below so `complete-milestone` can
				// re-run idempotently.
				if (!validationTerminal) {
					activeMilestone = { id: m.id, title };
					activeMilestoneSlices = slices;
					activeMilestoneFound = true;
					registry.push({
						id: m.id,
						title,
						status: "active",
						...(deps.length > 0 ? { dependsOn: deps } : {}),
					});
					continue;
				}
			}
			const contextFile = resolveMilestoneFile(basePath, m.id, "CONTEXT");
			const draftFile = resolveMilestoneFile(basePath, m.id, "CONTEXT-DRAFT");
			if (!contextFile && draftFile) activeMilestoneHasDraft = true;
			activeMilestone = { id: m.id, title };
			activeMilestoneSlices = slices;
			activeMilestoneFound = true;
			registry.push({
				id: m.id,
				title,
				status: "active",
				...(deps.length > 0 ? { dependsOn: deps } : {}),
			});
		} else {
			const deps = m.depends_on;
			registry.push({
				id: m.id,
				title,
				status: "pending",
				...(deps.length > 0 ? { dependsOn: deps } : {}),
			});
		}
	}
	if (!activeMilestoneFound && firstDeferredQueuedShell) {
		const shell = firstDeferredQueuedShell;
		activeMilestone = { id: shell.id, title: shell.title };
		activeMilestoneSlices = [];
		activeMilestoneFound = true;
		const entry = registry.find((e) => e.id === shell.id);
		if (entry) entry.status = "active";
	}
	return {
		registry,
		activeMilestone,
		activeMilestoneSlices,
		activeMilestoneHasDraft,
	};
}
function handleNoActiveMilestone(registry, requirements, milestoneProgress) {
	const pendingEntries = registry.filter((e) => e.status === "pending");
	const parkedEntries = registry.filter((e) => e.status === "parked");
	if (pendingEntries.length > 0) {
		const blockerDetails = pendingEntries
			.filter((e) => e.dependsOn && e.dependsOn.length > 0)
			.map(
				(e) => `${e.id} is waiting on unmet deps: ${e.dependsOn.join(", ")}`,
			);
		return {
			activeMilestone: null,
			activeSlice: null,
			activeTask: null,
			phase: "blocked",
			recentDecisions: [],
			blockers:
				blockerDetails.length > 0
					? blockerDetails
					: [
							"All remaining milestones are dep-blocked but no deps listed — check CONTEXT.md files",
						],
			nextAction: "Resolve milestone dependencies before proceeding.",
			registry,
			requirements,
			progress: { milestones: milestoneProgress },
		};
	}
	if (parkedEntries.length > 0) {
		const parkedIds = parkedEntries.map((e) => e.id).join(", ");
		return {
			activeMilestone: null,
			activeSlice: null,
			activeTask: null,
			phase: "pre-planning",
			recentDecisions: [],
			blockers: [],
			nextAction: `All remaining milestones are parked (${parkedIds}). Run /unpark <id> or create a new milestone.`,
			registry,
			requirements,
			progress: { milestones: milestoneProgress },
		};
	}
	if (registry.length === 0) {
		return {
			activeMilestone: null,
			activeSlice: null,
			activeTask: null,
			phase: "pre-planning",
			recentDecisions: [],
			blockers: [],
			nextAction: "No milestones found. Run /next to create one.",
			registry: [],
			requirements,
			progress: { milestones: { done: 0, total: 0 } },
		};
	}
	const lastEntry = registry[registry.length - 1];
	const activeReqs = requirements.active ?? 0;
	const completionNote =
		activeReqs > 0
			? `All milestones complete. ${activeReqs} active requirement${activeReqs === 1 ? "" : "s"} in REQUIREMENTS.md ${activeReqs === 1 ? "has" : "have"} not been mapped to a milestone.`
			: "All milestones complete.";
	return {
		activeMilestone: null,
		lastCompletedMilestone: lastEntry
			? { id: lastEntry.id, title: lastEntry.title }
			: null,
		activeSlice: null,
		activeTask: null,
		phase: "complete",
		recentDecisions: [],
		blockers: [],
		nextAction: completionNote,
		registry,
		requirements,
		progress: { milestones: milestoneProgress },
	};
}
async function handleAllSlicesDone(
	basePath,
	activeMilestone,
	registry,
	requirements,
	milestoneProgress,
	sliceProgress,
	activeMilestoneSlices,
) {
	// Requirements-aware completion gate (sf-mp74hftw-zud6ba).
	// Before the slice-based "real work" check, consult REQUIREMENTS.md:
	// if every owning requirement for this milestone is closed
	// (complete/done/skipped), the contract is satisfied regardless of
	// slice state. Route to completing-milestone (write SUMMARY) instead
	// of pre-planning, so a milestone whose work was tracked at the
	// requirements layer doesn't get re-decomposed every time its slice
	// state changes shape (e.g. someone skips a stale placeholder).
	try {
		const reqsByMs = await readRequirementsByMilestone(basePath);
		const owning = reqsByMs.get(activeMilestone.id);
		if (owning && owning.incomplete === 0 && owning.complete > 0) {
			// Route to validating-milestone, NOT completing-milestone:
			// the PDD purpose gate (ADR-0000) requires all 8 PDD fields
			// — Purpose, Consumer, Contract, Failure boundary, Evidence,
			// Non-goals, Invariants, Assumptions — to be present and
			// satisfied before a milestone closes. Requirements-complete
			// only covers the Contract field. The validate-milestone
			// unit is the one that checks the full PDD; sending the
			// milestone there honors the gate while still keeping it
			// out of pre-planning's re-decomposition trap.
			const { terminal: validationTerminal } =
				await readMilestoneValidationVerdict(
					basePath,
					activeMilestone.id,
					loadFile,
				);
			if (!validationTerminal) {
				return {
					activeMilestone,
					activeSlice: null,
					activeTask: null,
					phase: "validating-milestone",
					recentDecisions: [],
					blockers: [],
					nextAction:
						`All ${owning.complete} requirement(s) owned by ` +
						`${activeMilestone.id} are marked complete (Contract gate ` +
						`satisfied). Run validate-milestone to check the remaining ` +
						`PDD fields — Purpose, Consumer, Failure boundary, Evidence, ` +
						`Non-goals, Invariants, Assumptions — before closing.`,
					registry,
					requirements,
					progress: { milestones: milestoneProgress, slices: sliceProgress },
				};
			}
		}
	} catch {
		// Best-effort: requirements lookup must not break state derivation.
	}
	// All-slices-done collapses three quite different states (every
	// slice complete, every slice skipped, mix of both) into one
	// "ready to validate" signal. That's wrong when zero slices carry
	// real-work outcomes — typically the migration-placeholder pattern
	// where someone skipped the only slice on a milestone and SF
	// would then ask the operator to validate work that never
	// happened. Route those to reassessment instead so the milestone
	// gets real slices planned.
	const hasRealWork = Array.isArray(activeMilestoneSlices)
		? hasRealWorkSlice(activeMilestoneSlices)
		: true; // fall back to old behaviour if caller didn't pass slices
	if (!hasRealWork) {
		// Route into the pre-planning ladder. The dispatcher decides:
		//
		//   no CONTEXT file -> discuss-milestone
		//   has CONTEXT, no RESEARCH -> research-milestone
		//   has both -> plan-milestone (which decomposes into real
		//              slices alongside the existing skipped ones)
		//
		// The milestone's vision/PDD is already in the DB (set when
		// the milestone was created via `sf new-milestone`), so the
		// planner has the purpose it needs. No operator action
		// required - autonomous mode will pick it up on the next
		// dispatch tick.
		return buildNoRealWorkPrePlanningState(
			activeMilestone,
			registry,
			requirements,
			milestoneProgress,
			sliceProgress,
		);
	}
	const { terminal: validationTerminal, verdict } =
		await readMilestoneValidationVerdict(
			basePath,
			activeMilestone.id,
			loadFile,
		);
	if (!validationTerminal || verdict === "needs-remediation") {
		return {
			activeMilestone,
			activeSlice: null,
			activeTask: null,
			phase: "validating-milestone",
			recentDecisions: [],
			blockers: [],
			nextAction: `Validate milestone ${activeMilestone.id} before completion.`,
			registry,
			requirements,
			progress: { milestones: milestoneProgress, slices: sliceProgress },
		};
	}
	return {
		activeMilestone,
		activeSlice: null,
		activeTask: null,
		phase: "completing-milestone",
		recentDecisions: [],
		blockers: [],
		nextAction: `All slices complete in ${activeMilestone.id}. Write milestone summary.`,
		registry,
		requirements,
		progress: { milestones: milestoneProgress, slices: sliceProgress },
	};
}
function resolveSliceDependencies(activeMilestoneSlices) {
	const doneSliceIds = new Set(
		activeMilestoneSlices
			.filter((s) => isStatusDone(s.status))
			.map((s) => s.id),
	);
	const sliceLock = process.env.SF_SLICE_LOCK;
	if (sliceLock) {
		const lockedSlice = activeMilestoneSlices.find((s) => s.id === sliceLock);
		if (lockedSlice && !isInactiveStatus(lockedSlice.status)) {
			return {
				activeSlice: { id: lockedSlice.id, title: lockedSlice.title },
				activeSliceRow: lockedSlice,
			};
		} else {
			logWarning(
				"state",
				`SF_SLICE_LOCK=${sliceLock} not found in active slices — worker has no assigned work`,
			);
			return { activeSlice: null, activeSliceRow: null };
		}
	}
	// First pass: find a slice with ALL dependencies satisfied (strict)
	let bestFallback = null;
	let bestFallbackSatisfied = -1;
	for (const s of activeMilestoneSlices) {
		if (isInactiveStatus(s.status)) continue;
		if (s.depends.every((dep) => doneSliceIds.has(dep))) {
			return { activeSlice: { id: s.id, title: s.title }, activeSliceRow: s };
		}
		// Track the slice with the most satisfied dependencies as fallback
		const satisfied = s.depends.filter((dep) => doneSliceIds.has(dep)).length;
		if (
			satisfied > bestFallbackSatisfied ||
			(satisfied === bestFallbackSatisfied && !bestFallback)
		) {
			bestFallback = s;
			bestFallbackSatisfied = satisfied;
		}
	}
	// Fallback: if no slice has all deps met but there ARE incomplete non-deferred
	// slices, pick the one with the most deps satisfied. This prevents hard-blocking
	// when dependency metadata is stale (e.g. after reassessment added/removed slices)
	// or when deps reference slices from previous milestones.
	if (bestFallback) {
		const unmet = bestFallback.depends.filter((dep) => !doneSliceIds.has(dep));
		logWarning(
			"state",
			`No slice has all deps satisfied — falling back to ${bestFallback.id} ` +
				`(${bestFallbackSatisfied}/${bestFallback.depends.length} deps met, ` +
				`unmet: ${unmet.join(", ")})`,
			{ mid: activeMilestoneSlices[0]?.milestone_id, sid: bestFallback.id },
		);
		return {
			activeSlice: { id: bestFallback.id, title: bestFallback.title },
			activeSliceRow: bestFallback,
		};
	}
	return { activeSlice: null, activeSliceRow: null };
}

function hasRealWorkSlice(slices) {
	return slices.some((s) => s.status === "complete" || s.status === "done");
}

function buildNoRealWorkPrePlanningState(
	activeMilestone,
	registry,
	requirements,
	milestoneProgress,
	sliceProgress,
) {
	return {
		activeMilestone,
		activeSlice: null,
		activeTask: null,
		phase: "pre-planning",
		recentDecisions: [],
		blockers: [],
		nextAction:
			`Milestone ${activeMilestone.id} has no slice carrying real ` +
			`work (every slice is skipped, cancelled, or otherwise inactive). ` +
			`Route into the pre-planning ladder so the dispatcher picks ` +
			`discuss/research/plan based on which artifacts are missing. Use ` +
			`\`sf headless complete-milestone ${activeMilestone.id}\` ` +
			`only if you want to intentionally defer the work without ` +
			`planning it.`,
		registry,
		requirements,
		progress: { milestones: milestoneProgress, slices: sliceProgress },
	};
}

function hasSummaryCompletionMarker(md) {
	return (
		typeof md === "string" &&
		/^---[\s\S]*?\bcompleted_at\s*:/m.test(md.trimStart())
	);
}

async function hasParseableTaskSummary(basePath, milestoneId, sliceId, taskId) {
	const summaryPath = resolveTaskFile(
		basePath,
		milestoneId,
		sliceId,
		taskId,
		"SUMMARY",
	);
	if (!summaryPath || !existsSync(summaryPath)) return false;
	const content = await loadFile(summaryPath);
	if (!isValidTaskSummary(content)) return false;
	try {
		parseSummary(content);
		return true;
	} catch {
		return false;
	}
}

function recordStatusCompletionDrift(basePath, milestoneId, sliceId, task) {
	insertSelfFeedbackEntry({
		id: `sf-${Date.now().toString(36)}-${randomUUID().slice(0, 8)}`,
		ts: new Date().toISOString(),
		kind: "status-completion-drift",
		severity: "high",
		blocking: true,
		repoIdentity: "forge",
		sfVersion: "unknown",
		basePath,
		occurredIn: {
			unitType: "execute-task",
			milestone: milestoneId,
			slice: sliceId,
			task: task.id,
		},
		summary: `Quarantined ${milestoneId}/${sliceId}/${task.id}: prose/evidence says complete while status is ${task.status}.`,
		evidence:
			`status=${task.status}; task_status=${task.task_status}; ` +
			`verification_status=${task.verification_status}; completed_at=${task.completed_at ?? ""}`,
		suggestedFix:
			"Reconcile the row with completeTaskAtomic or reopen the task before dispatch.",
	});
}

async function reconcileSliceTasks(basePath, milestoneId, sliceId, planFile) {
	const tasks = getSliceTasks(milestoneId, sliceId);
	if (tasks.length === 0 && planFile) {
		logWarning(
			"reconcile",
			`slice plan file exists for ${milestoneId}/${sliceId}, but DB has no task rows; refusing runtime import`,
			{ mid: milestoneId, sid: sliceId },
		);
	}
	const reconciled = [];
	for (const t of tasks) {
		if (isStatusDone(t.status)) {
			reconciled.push(t);
			continue;
		}
		const hasProseCompletion =
			(await hasParseableTaskSummary(basePath, milestoneId, sliceId, t.id)) ||
			hasSummaryCompletionMarker(t.full_summary_md) ||
			t.verification_status === "all_pass";
		if (hasProseCompletion) {
			recordStatusCompletionDrift(basePath, milestoneId, sliceId, t);
			logWarning(
				"reconcile",
				`task ${milestoneId}/${sliceId}/${t.id} has completion evidence but DB status is "${t.status}"; quarantining from dispatch until the row is reconciled explicitly.`,
				{ mid: milestoneId, sid: sliceId, tid: t.id },
			);
			reconciled.push({ ...t, _quarantined_drift: true });
			continue;
		}
		reconciled.push(t);
	}
	return reconciled;
}
async function detectBlockers(basePath, milestoneId, sliceId, tasks) {
	const completedTasks = tasks.filter((t) => isStatusDone(t.status));
	for (const ct of completedTasks) {
		if (ct.blocker_discovered) {
			return ct.id;
		}
		const summaryFile = resolveTaskFile(
			basePath,
			milestoneId,
			sliceId,
			ct.id,
			"SUMMARY",
		);
		if (!summaryFile) continue;
		const summaryContent = await loadFile(summaryFile);
		if (!summaryContent) continue;
		const summary = parseSummary(summaryContent);
		if (summary.frontmatter.blocker_discovered) {
			return ct.id;
		}
	}
	return null;
}
function checkReplanTrigger(basePath, milestoneId, sliceId) {
	const sliceRow = getSlice(milestoneId, sliceId);
	const dbTriggered = !!sliceRow?.replan_triggered_at;
	const diskTriggered =
		!dbTriggered &&
		!!resolveSliceFile(basePath, milestoneId, sliceId, "REPLAN-TRIGGER");
	return dbTriggered || diskTriggered;
}
async function checkInterruptedWork(basePath, milestoneId, sliceId) {
	const sDir = resolveSlicePath(basePath, milestoneId, sliceId);
	const continueFile = sDir
		? resolveSliceFile(basePath, milestoneId, sliceId, "CONTINUE")
		: null;
	return (
		!!(continueFile && (await loadFile(continueFile))) ||
		!!(sDir && (await loadFile(join(sDir, "continue.md"))))
	);
}
export async function deriveStateFromDb(basePath) {
	const requirements = parseRequirementCounts(
		await loadFile(resolveSfRootFile(basePath, "REQUIREMENTS")),
	);
	const allMilestones = reconcileDiskToDb(basePath);
	const customOrder = loadQueueOrder(basePath);
	const sortedIds = sortByQueueOrder(
		allMilestones.map((m) => m.id),
		customOrder,
	);
	const byId = new Map(allMilestones.map((m) => [m.id, m]));
	allMilestones.length = 0;
	for (const id of sortedIds) allMilestones.push(byId.get(id));
	const milestoneLock = process.env.SF_MILESTONE_LOCK;
	const milestones = milestoneLock
		? allMilestones.filter((m) => m.id === milestoneLock)
		: allMilestones;
	if (milestones.length === 0) {
		return {
			activeMilestone: null,
			activeSlice: null,
			activeTask: null,
			phase: "pre-planning",
			recentDecisions: [],
			blockers: [],
			nextAction: "No milestones found. Run /next to create one.",
			registry: [],
			requirements,
			progress: { milestones: { done: 0, total: 0 } },
		};
	}
	const { completeMilestoneIds, parkedMilestoneIds } = buildCompletenessSet(
		basePath,
		milestones,
	);
	const registryContext = await buildRegistryAndFindActive(
		basePath,
		milestones,
		completeMilestoneIds,
		parkedMilestoneIds,
	);
	const {
		registry,
		activeMilestone,
		activeMilestoneSlices,
		activeMilestoneHasDraft,
	} = registryContext;
	const milestoneProgress = {
		done: registry.filter((e) => e.status === "complete").length,
		total: registry.length,
	};
	if (!activeMilestone) {
		return handleNoActiveMilestone(registry, requirements, milestoneProgress);
	}
	const hasRoadmap =
		resolveMilestoneFile(basePath, activeMilestone.id, "ROADMAP") !== null;
	if (activeMilestoneSlices.length === 0) {
		if (!hasRoadmap) {
			const phase = activeMilestoneHasDraft
				? "needs-discussion"
				: "pre-planning";
			const nextAction = activeMilestoneHasDraft
				? `Discuss draft context for milestone ${activeMilestone.id}.`
				: `Plan milestone ${activeMilestone.id}.`;
			return {
				activeMilestone,
				activeSlice: null,
				activeTask: null,
				phase,
				recentDecisions: [],
				blockers: [],
				nextAction,
				registry,
				requirements,
				progress: { milestones: milestoneProgress },
			};
		}
		return {
			activeMilestone,
			activeSlice: null,
			activeTask: null,
			phase: "pre-planning",
			recentDecisions: [],
			blockers: [],
			nextAction: `Milestone ${activeMilestone.id} has a roadmap but no slices defined. Add slices to the roadmap.`,
			registry,
			requirements,
			progress: {
				milestones: milestoneProgress,
				slices: { done: 0, total: 0 },
			},
		};
	}
	const allSlicesDone = activeMilestoneSlices.every((s) =>
		isStatusDone(s.status),
	);
	const sliceProgress = {
		done: activeMilestoneSlices.filter((s) => isStatusDone(s.status)).length,
		total: activeMilestoneSlices.length,
	};
	if (allSlicesDone) {
		return handleAllSlicesDone(
			basePath,
			activeMilestone,
			registry,
			requirements,
			milestoneProgress,
			sliceProgress,
			activeMilestoneSlices,
		);
	}
	const activeSliceContext = resolveSliceDependencies(activeMilestoneSlices);
	if (!activeSliceContext.activeSlice) {
		// If locked slice wasn't found, it returns null but logs warning, we need to return 'blocked'
		if (process.env.SF_SLICE_LOCK) {
			return {
				activeMilestone,
				activeSlice: null,
				activeTask: null,
				phase: "blocked",
				recentDecisions: [],
				blockers: [
					`SF_SLICE_LOCK=${process.env.SF_SLICE_LOCK} not found in active milestone slices`,
				],
				nextAction:
					"Slice lock references a non-existent slice — check orchestrator dispatch.",
				registry,
				requirements,
				progress: { milestones: milestoneProgress, slices: sliceProgress },
			};
		}
		if (
			activeMilestoneSlices.length > 0 &&
			!hasRealWorkSlice(activeMilestoneSlices)
		) {
			return buildNoRealWorkPrePlanningState(
				activeMilestone,
				registry,
				requirements,
				milestoneProgress,
				sliceProgress,
			);
		}
		return {
			activeMilestone,
			activeSlice: null,
			activeTask: null,
			phase: "blocked",
			recentDecisions: [],
			blockers: ["No slice eligible — check dependency ordering"],
			nextAction: "Resolve dependency blockers or plan next slice.",
			registry,
			requirements,
			progress: { milestones: milestoneProgress, slices: sliceProgress },
		};
	}
	const { activeSlice } = activeSliceContext;
	const planFile = resolveSliceFile(
		basePath,
		activeMilestone.id,
		activeSlice.id,
		"PLAN",
	);
	const dbTasksBefore = getSliceTasks(activeMilestone.id, activeSlice.id);
	if (!planFile && dbTasksBefore.length === 0) {
		return {
			activeMilestone,
			activeSlice,
			activeTask: null,
			phase: "planning",
			recentDecisions: [],
			blockers: [],
			nextAction: `Plan slice ${activeSlice.id} (${activeSlice.title}).`,
			registry,
			requirements,
			progress: { milestones: milestoneProgress, slices: sliceProgress },
		};
	}
	const tasks = planFile
		? await reconcileSliceTasks(
				basePath,
				activeMilestone.id,
				activeSlice.id,
				planFile,
			)
		: dbTasksBefore;
	const taskProgress = {
		done: tasks.filter((t) => isStatusDone(t.status)).length,
		total: tasks.length,
	};
	const activeTaskRow = tasks.find(
		(t) => !isStatusDone(t.status) && !t._quarantined_drift,
	);
	const quarantinedDriftTasks = tasks.filter((t) => t._quarantined_drift);
	if (!activeTaskRow && quarantinedDriftTasks.length > 0) {
		return {
			activeMilestone,
			activeSlice,
			activeTask: null,
			phase: "blocked",
			recentDecisions: [],
			blockers: quarantinedDriftTasks.map(
				(t) =>
					`Task ${t.id} is quarantined: prose/evidence says complete while status is ${t.status}`,
			),
			nextAction: `Reconcile quarantined task drift in ${activeSlice.id} before dispatch.`,
			registry,
			requirements,
			progress: {
				milestones: milestoneProgress,
				slices: sliceProgress,
				tasks: taskProgress,
			},
		};
	}
	if (!activeTaskRow && tasks.length > 0) {
		return {
			activeMilestone,
			activeSlice,
			activeTask: null,
			phase: "summarizing",
			recentDecisions: [],
			blockers: [],
			nextAction: `All tasks done in ${activeSlice.id}. Write slice summary and complete slice.`,
			registry,
			requirements,
			progress: {
				milestones: milestoneProgress,
				slices: sliceProgress,
				tasks: taskProgress,
			},
		};
	}
	if (!activeTaskRow) {
		return {
			activeMilestone,
			activeSlice,
			activeTask: null,
			phase: "planning",
			recentDecisions: [],
			blockers: [],
			nextAction: `Slice ${activeSlice.id} has a plan file but no tasks. Add tasks to the plan.`,
			registry,
			requirements,
			progress: {
				milestones: milestoneProgress,
				slices: sliceProgress,
				tasks: taskProgress,
			},
		};
	}
	const activeTask = {
		id: activeTaskRow.id,
		title: activeTaskRow.title,
	};
	const tasksDir = resolveTasksDir(
		basePath,
		activeMilestone.id,
		activeSlice.id,
	);
	if (tasksDir && existsSync(tasksDir) && tasks.length > 0) {
		const allFiles = readdirSync(tasksDir).filter((f) => f.endsWith(".md"));
		if (allFiles.length === 0) {
			return {
				activeMilestone,
				activeSlice,
				activeTask: null,
				phase: "planning",
				recentDecisions: [],
				blockers: [],
				nextAction: `Task plan files missing for ${activeSlice.id}. Run plan-slice to generate task plans.`,
				registry,
				requirements,
				progress: {
					milestones: milestoneProgress,
					slices: sliceProgress,
					tasks: taskProgress,
				},
			};
		}
	}
	// ── Quality gate evaluation check ──────────────────────────────────
	// Pause before execution only when gates owned by the `gate-evaluate`
	// turn (Q3/Q4) are still pending. Q8 is also `scope:"slice"` but is
	// owned by `complete-slice`, so it must NOT block the evaluating-gates
	// phase — otherwise auto-loop stalls forever waiting for a gate that
	// this turn never evaluates. See gate-registry.ts for the ownership map.
	// Slices with zero gate rows (pre-feature or simple) skip straight through.
	const pendingGateCount = getPendingGateCountForTurn(
		activeMilestone.id,
		activeSlice.id,
		"gate-evaluate",
	);
	if (pendingGateCount > 0) {
		return {
			activeMilestone,
			activeSlice,
			activeTask: null,
			phase: "evaluating-gates",
			recentDecisions: [],
			blockers: [],
			nextAction: `Evaluate ${pendingGateCount} quality gate(s) for ${activeSlice.id} before execution.`,
			registry,
			requirements,
			progress: {
				milestones: milestoneProgress,
				slices: sliceProgress,
				tasks: taskProgress,
			},
		};
	}
	const blockerTaskId = await detectBlockers(
		basePath,
		activeMilestone.id,
		activeSlice.id,
		tasks,
	);
	if (blockerTaskId) {
		const replanHistory = getReplanHistory(activeMilestone.id, activeSlice.id);
		if (replanHistory.length === 0) {
			return {
				activeMilestone,
				activeSlice,
				activeTask,
				phase: "replanning-slice",
				recentDecisions: [],
				blockers: [
					`Task ${blockerTaskId} discovered a blocker requiring slice replan`,
				],
				nextAction: `Task ${blockerTaskId} reported blocker_discovered. Replan slice ${activeSlice.id} before continuing.`,
				activeWorkspace: undefined,
				registry,
				requirements,
				progress: {
					milestones: milestoneProgress,
					slices: sliceProgress,
					tasks: taskProgress,
				},
			};
		}
	}
	if (!blockerTaskId) {
		const isTriggered = checkReplanTrigger(
			basePath,
			activeMilestone.id,
			activeSlice.id,
		);
		if (isTriggered) {
			const replanHistory = getReplanHistory(
				activeMilestone.id,
				activeSlice.id,
			);
			if (replanHistory.length === 0) {
				return {
					activeMilestone,
					activeSlice,
					activeTask,
					phase: "replanning-slice",
					recentDecisions: [],
					blockers: ["Triage replan trigger detected — slice replan required"],
					nextAction: `Triage replan triggered for slice ${activeSlice.id}. Replan before continuing.`,
					activeWorkspace: undefined,
					registry,
					requirements,
					progress: {
						milestones: milestoneProgress,
						slices: sliceProgress,
						tasks: taskProgress,
					},
				};
			}
		}
	}
	const hasInterrupted = await checkInterruptedWork(
		basePath,
		activeMilestone.id,
		activeSlice.id,
	);
	return {
		activeMilestone,
		activeSlice,
		activeTask,
		phase: "executing",
		recentDecisions: [],
		blockers: [],
		nextAction: hasInterrupted
			? `Resume interrupted work on ${activeTask.id}: ${activeTask.title} in slice ${activeSlice.id}. Read continue.md first.`
			: `Execute ${activeTask.id}: ${activeTask.title} in slice ${activeSlice.id}.`,
		registry,
		requirements,
		progress: {
			milestones: milestoneProgress,
			slices: sliceProgress,
			tasks: taskProgress,
		},
	};
}
// LEGACY: Filesystem-based state derivation for unmigrated projects.
// DB-backed projects use deriveStateFromDb() above. Target: extract to
// state-legacy.ts when all projects are DB-backed.
