fix: reduce stale doctor warnings

This commit is contained in:
Mikael Hugo 2026-05-05 22:46:13 +02:00
parent e32d620cc5
commit 969b0f3295
5 changed files with 63 additions and 20 deletions

View file

@ -1,7 +1,12 @@
import { existsSync, readdirSync, renameSync, rmSync, statSync } from "node:fs";
import { join } from "node:path";
import { milestonesDir, resolveMilestoneFile } from "./paths.js";
import { _getAdapter, getAllMilestones, isDbAvailable } from "./sf-db.js";
import {
_getAdapter,
getAllMilestones,
isDbAvailable,
openDatabase,
} from "./sf-db.js";
import { deriveState } from "./state.js";
import { readEvents } from "./workflow-events.js";
import { renderAllProjections } from "./workflow-projections.js";
@ -28,18 +33,17 @@ function normalizeLegacyDirectory(
const sourcePath = join(parentDir, entry);
const targetPath = join(parentDir, bareId);
const conflict = existsSync(targetPath);
if (conflict) return;
issues.push({
severity: conflict ? "warning" : "info",
severity: "info",
code: "legacy_plan_slug_directory",
scope: "project",
unitId: `${unitPrefix}/${entry}`,
message: conflict
? `Legacy plan directory ${sourcePath} should be renamed to ${targetPath}, but the target already exists. Merge manually before running doctor fix.`
: `Legacy plan directory ${sourcePath} should be renamed to bare ID directory ${targetPath}.`,
message: `Legacy plan directory ${sourcePath} should be renamed to bare ID directory ${targetPath}.`,
file: sourcePath,
fixable: !conflict,
fixable: true,
});
if (conflict || !shouldFix?.("legacy_plan_slug_directory")) return;
if (!shouldFix?.("legacy_plan_slug_directory")) return;
try {
renameSync(sourcePath, targetPath);
fixesApplied.push(
@ -105,6 +109,14 @@ export async function checkEngineHealth(
shouldFix,
) {
const dbPath = join(basePath, ".sf", "sf.db");
if (!isDbAvailable() && existsSync(dbPath)) {
try {
openDatabase(dbPath);
} catch {
// Report below. Doctor must not throw just because the engine DB is
// locked, corrupt, or unavailable in this process.
}
}
if (!isDbAvailable() && existsSync(dbPath)) {
issues.push({
severity: "warning",

View file

@ -543,7 +543,7 @@ function checkGitRemote(basePath) {
const remote = tryExec("git remote get-url origin", basePath);
if (!remote) return null;
// Quick connectivity check with short timeout
const result = tryExec("git ls-remote --exit-code -h origin HEAD", basePath);
const result = tryExec("git ls-remote --exit-code origin HEAD", basePath);
if (result === null) {
return {
name: "git_remote",

View file

@ -704,8 +704,6 @@ function formatBucketCountParts(counts) {
parts.push(`${counts.upgradable} pending upgrade`);
if (counts["editing-drift"] && counts["editing-drift"] > 0)
parts.push(`${counts["editing-drift"]} editing-drift`);
if (counts.untracked && counts.untracked > 0)
parts.push(`${counts.untracked} untracked`);
const pendingCount = (counts.missing ?? 0) + (counts.upgradable ?? 0);
return { parts, pendingCount };
}
@ -727,10 +725,7 @@ export function checkScaffoldFreshness(basePath) {
}
const counts = report.countsByBucket;
const actionable =
counts.missing +
counts.upgradable +
counts["editing-drift"] +
counts.untracked;
counts.missing + counts.upgradable + counts["editing-drift"];
if (actionable === 0) return null;
const { parts, pendingCount } = formatBucketCountParts(counts);
const summary = parts.join(", ");

View file

@ -9,7 +9,11 @@ import {
import { tmpdir } from "node:os";
import { dirname, join } from "node:path";
import { afterEach, test } from "vitest";
import { ensureAgenticDocsScaffold } from "../agentic-docs-scaffold.js";
import {
ensureAgenticDocsScaffold,
SCAFFOLD_FILES,
} from "../agentic-docs-scaffold.js";
import { checkScaffoldFreshness } from "../doctor-runtime-checks.js";
import { detectScaffoldDrift } from "../scaffold-drift.js";
import { extractMarker, stampScaffoldFile } from "../scaffold-versioning.js";
@ -131,3 +135,14 @@ All lines should start with \`OK:\` for the bootstrap spec to pass.
assert.equal(existsSync(join(root, "harness")), false);
assert.equal(existsSync(join(root, ".sf/harness/specs/bootstrap.md")), true);
});
test("checkScaffoldFreshness_when_only_markerless_scaffold_exists_does_not_warn", () => {
const root = makeProject();
for (const file of SCAFFOLD_FILES) {
const target = join(root, file.path);
mkdirSync(dirname(target), { recursive: true });
writeFileSync(target, file.content, "utf-8");
}
assert.equal(checkScaffoldFreshness(root), null);
});

View file

@ -9,7 +9,11 @@ import {
import { tmpdir } from "node:os";
import { join } from "node:path";
import { afterEach, describe, test } from "vitest";
import { normalizeLegacyPlanSlugDirectories } from "../doctor-engine-checks.js";
import {
checkEngineHealth,
normalizeLegacyPlanSlugDirectories,
} from "../doctor-engine-checks.js";
import { closeDatabase, openDatabase } from "../sf-db.js";
const tmpDirs = [];
@ -21,6 +25,7 @@ function makeProject() {
}
afterEach(() => {
closeDatabase();
while (tmpDirs.length > 0) {
const dir = tmpDirs.pop();
if (dir) rmSync(dir, { recursive: true, force: true });
@ -96,15 +101,31 @@ describe("doctor plan directory normalization", () => {
(code) => code === "legacy_plan_slug_directory",
);
const issue = issues.find(
(candidate) => candidate.code === "legacy_plan_slug_directory",
assert.equal(
issues.some(
(candidate) => candidate.code === "legacy_plan_slug_directory",
),
false,
);
assert.equal(issue?.fixable, false);
assert.match(issue?.message ?? "", /target already exists/);
assert.equal(
existsSync(join(project, ".sf", "milestones", "M001-long-name")),
true,
);
assert.deepEqual(fixesApplied, []);
});
test("checkEngineHealth_when_db_file_can_open_does_not_report_unavailable", async () => {
const project = makeProject();
const dbPath = join(project, ".sf", "sf.db");
assert.equal(openDatabase(dbPath), true);
closeDatabase();
const issues = [];
await checkEngineHealth(project, issues, [], () => false);
assert.equal(
issues.some((issue) => issue.code === "db_unavailable"),
false,
);
});
});