singularity-forge/src/resources/extensions/sf/state-shared.js
Mikael Hugo 0aaf8f2c0e refactor: split state.js into state-shared/db/legacy modules
state.js was a 2012-line monolith combining shared helpers, DB-backed
derivation, and legacy filesystem derivation. Split into four files:

- state-shared.js (114 lines): helpers used by both DB and legacy paths
  isGhostMilestone, isSliceComplete, isMilestoneComplete, isValidationTerminal,
  readMilestoneValidationVerdict, loadTerminalSummary, stripMilestonePrefix,
  canonicalMilestonePrefix, extractContextTitle

- state-db.js (841 lines): deriveStateFromDb() and its exclusive helpers
  reconcileDiskToDb, buildRegistryAndFindActive, handleNoActiveMilestone,
  handleAllSlicesDone, resolveSliceDependencies, reconcileSliceTasks,
  detectBlockers, checkReplanTrigger, checkInterruptedWork

- state-legacy.js (895 lines): _deriveStateImpl() — filesystem-only path

- state.js (228 lines): thin barrel — invalidateStateCache, getActiveMilestoneId,
  deriveState, re-exports from sub-modules

All 1195 tests pass. No behavior change.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-05-11 16:25:20 +02:00

114 lines
4.4 KiB
JavaScript

// SF Extension — State Shared Helpers
// Helpers shared by both the DB-backed (state-db.js) and legacy filesystem
// (state-legacy.js) state derivation paths. No dependency on either.
import { existsSync } from "node:fs";
import { join } from "node:path";
import { isTerminalMilestoneSummaryContent } from "./milestone-summary-classifier.js";
import { resolveMilestoneFile, sfRoot } from "./paths.js";
import {
getMilestone,
getMilestoneValidationAssessment,
isDbAvailable,
} from "./sf-db.js";
import { extractVerdict } from "./verdict-parser.js";
export function isGhostMilestone(basePath, mid) {
// If the milestone has a DB row, it's usually a known milestone — not a ghost.
// Exception: a "queued" row with no disk artifacts is a phantom from
// new_milestone_id that was never planned (#3645).
if (isDbAvailable()) {
const dbRow = getMilestone(mid);
if (dbRow) {
if (dbRow.status === "queued") {
const hasContent =
resolveMilestoneFile(basePath, mid, "CONTEXT") ||
resolveMilestoneFile(basePath, mid, "CONTEXT-DRAFT") ||
resolveMilestoneFile(basePath, mid, "ROADMAP") ||
resolveMilestoneFile(basePath, mid, "SUMMARY");
return !hasContent;
}
return false;
}
}
// If a worktree exists for this milestone, it was legitimately created.
const root = sfRoot(basePath);
const wtPath = join(root, "worktrees", mid);
if (existsSync(wtPath)) return false;
// Fall back to content-file check: no substantive files means ghost.
const context = resolveMilestoneFile(basePath, mid, "CONTEXT");
const draft = resolveMilestoneFile(basePath, mid, "CONTEXT-DRAFT");
const roadmap = resolveMilestoneFile(basePath, mid, "ROADMAP");
const summary = resolveMilestoneFile(basePath, mid, "SUMMARY");
return !context && !draft && !roadmap && !summary;
}
// ─── Query Functions ───────────────────────────────────────────────────────
/**
* Check if all tasks in a slice plan are done.
*/
export function isSliceComplete(plan) {
return plan.tasks.length > 0 && plan.tasks.every((t) => t.done);
}
/**
* Check if all slices in a roadmap are done.
*/
export function isMilestoneComplete(roadmap) {
return roadmap.slices.length > 0 && roadmap.slices.every((s) => s.done);
}
/**
* Check whether a VALIDATION file's verdict is terminal.
* Any successfully extracted verdict (pass, needs-attention, needs-remediation,
* fail, etc.) means validation completed. Only return false when no verdict
* could be parsed — i.e. extractVerdict() returns undefined (#2769).
*/
export function isValidationTerminal(validationContent) {
return extractVerdict(validationContent) != null;
}
function getDbMilestoneValidationVerdict(milestoneId) {
if (!isDbAvailable()) return undefined;
const assessment = getMilestoneValidationAssessment(milestoneId);
const status = assessment?.status;
return typeof status === "string" && status.trim()
? status.trim().toLowerCase()
: undefined;
}
export async function readMilestoneValidationVerdict(basePath, milestoneId, load) {
const dbVerdict = getDbMilestoneValidationVerdict(milestoneId);
if (dbVerdict) {
return { terminal: true, verdict: dbVerdict };
}
if (isDbAvailable()) {
return { terminal: false, verdict: undefined, source: "db-missing" };
}
const validationFile = resolveMilestoneFile(
basePath,
milestoneId,
"VALIDATION",
);
const validationContent = validationFile ? await load(validationFile) : null;
return {
terminal: validationContent
? isValidationTerminal(validationContent)
: false,
verdict: validationContent ? extractVerdict(validationContent) : undefined,
};
}
export async function loadTerminalSummary(summaryFile, loadFn) {
if (!summaryFile) return null;
const sc = await loadFn(summaryFile);
if (sc == null || !isTerminalMilestoneSummaryContent(sc)) return null;
return sc;
}
export function stripMilestonePrefix(title) {
return title.replace(/^M\d+(?:-[a-z0-9]{6})?[^:]*:\s*/, "") || title;
}
export function canonicalMilestonePrefix(id) {
return id.match(/^([A-Z]\d{3})/)?.[1] ?? id;
}
export function extractContextTitle(content, fallback) {
if (!content) return fallback;
const h1 = content.split("\n").find((line) => line.startsWith("# "));
if (!h1) return fallback;
// Extract title from "# M005: Platform Foundation & Separation" format
return stripMilestonePrefix(h1.slice(2).trim()) || fallback;
}