fix: clean generated harness residue
This commit is contained in:
parent
2d9c2018af
commit
55e7dd0e02
3 changed files with 203 additions and 1 deletions
35
docs/dev/generated-artifact-policy.md
Normal file
35
docs/dev/generated-artifact-policy.md
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
# Generated Artifact Policy
|
||||
|
||||
SF keeps operational generated artifacts out of the project root unless a human
|
||||
explicitly promotes them into durable project documentation.
|
||||
|
||||
## Default Locations
|
||||
|
||||
- `.sf/` stores SF-local operational state, generated harness notes, scaffold
|
||||
manifests, runtime caches, locks, and temporary agent files.
|
||||
- `docs/plans/`, `docs/specs/`, and `docs/adr/` store promoted, reviewed,
|
||||
durable project artifacts.
|
||||
- Root files such as `AGENTS.md`, `ARCHITECTURE.md`, and `.siftignore` are
|
||||
allowed only when they are part of the versioned scaffold contract.
|
||||
|
||||
## Harness
|
||||
|
||||
Generated harness material belongs under `.sf/harness/`.
|
||||
|
||||
Top-level `harness/` is not created by SF by default. If a project intentionally
|
||||
owns a top-level harness, SF leaves it alone unless the files still carry
|
||||
pending `sf-doc` markers proving they are old SF-generated residue.
|
||||
|
||||
## Doctor Cleanup
|
||||
|
||||
`/sf doctor --fix` may remove root-level generated residue only when all of the
|
||||
following are true:
|
||||
|
||||
- the file is under a legacy SF-generated path such as `harness/specs/`;
|
||||
- the file has an `sf-doc` marker;
|
||||
- the marker template matches the file path;
|
||||
- the marker state is `pending`;
|
||||
- the current body hash still matches the marker hash.
|
||||
|
||||
Anything edited, completed, unmarked, or project-owned is reported conservatively
|
||||
or left untouched.
|
||||
|
|
@ -4,8 +4,9 @@ import {
|
|||
mkdirSync,
|
||||
readdirSync,
|
||||
readFileSync,
|
||||
rmSync,
|
||||
} from "node:fs";
|
||||
import { join } from "node:path";
|
||||
import { dirname, join } from "node:path";
|
||||
import { invalidateAllCaches } from "./cache.js";
|
||||
import {
|
||||
checkEngineHealth,
|
||||
|
|
@ -42,6 +43,7 @@ import {
|
|||
sfRoot,
|
||||
} from "./paths.js";
|
||||
import { loadEffectiveSFPreferences } from "./preferences.js";
|
||||
import { bodyHash, extractMarker } from "./scaffold-versioning.js";
|
||||
import { readAllSelfFeedback, recordSelfFeedback } from "./self-feedback.js";
|
||||
import { getMilestoneSlices, getSliceTasks, isDbAvailable } from "./sf-db.js";
|
||||
import { deriveState, isMilestoneComplete } from "./state.js";
|
||||
|
|
@ -53,6 +55,79 @@ const DEFAULT_STALE_PROGRESS_MS = 20 * 60 * 1000;
|
|||
const DEFAULT_OPTIONAL_CHILD_BUDGET_MS = 30 * 60 * 1000;
|
||||
const REPEATED_FAILURE_THRESHOLD = 3;
|
||||
const FLOW_AUDIT_ROLLUP_KIND = "flow-audit:repeated-milestone-failure";
|
||||
const LEGACY_ROOT_HARNESS_PATHS = [
|
||||
"harness/AGENTS.md",
|
||||
"harness/specs/AGENTS.md",
|
||||
"harness/specs/bootstrap.md",
|
||||
"harness/evals/AGENTS.md",
|
||||
"harness/graders/AGENTS.md",
|
||||
];
|
||||
|
||||
function pruneEmptyDir(path) {
|
||||
try {
|
||||
if (existsSync(path) && readdirSync(path).length === 0) {
|
||||
rmSync(path, { recursive: false });
|
||||
}
|
||||
} catch {
|
||||
// Best-effort cleanup only.
|
||||
}
|
||||
}
|
||||
|
||||
function collectOwnedLegacyRootHarnessFiles(basePath) {
|
||||
const owned = [];
|
||||
for (const relPath of LEGACY_ROOT_HARNESS_PATHS) {
|
||||
const target = join(basePath, relPath);
|
||||
if (!existsSync(target)) continue;
|
||||
const { marker, body } = extractMarker(target);
|
||||
if (!marker) continue;
|
||||
if (marker.template !== relPath) continue;
|
||||
if (marker.state !== "pending") continue;
|
||||
if (bodyHash(body) !== marker.hash) continue;
|
||||
owned.push(relPath);
|
||||
}
|
||||
return owned;
|
||||
}
|
||||
|
||||
function removeOwnedLegacyRootHarnessFiles(basePath, relPaths) {
|
||||
for (const relPath of relPaths) {
|
||||
rmSync(join(basePath, relPath), { force: true });
|
||||
}
|
||||
for (const relPath of relPaths) {
|
||||
let dir = dirname(join(basePath, relPath));
|
||||
while (dir.startsWith(join(basePath, "harness"))) {
|
||||
pruneEmptyDir(dir);
|
||||
if (dir === join(basePath, "harness")) break;
|
||||
dir = dirname(dir);
|
||||
}
|
||||
}
|
||||
pruneEmptyDir(join(basePath, "harness"));
|
||||
}
|
||||
|
||||
function checkGeneratedArtifactResidue(
|
||||
basePath,
|
||||
issues,
|
||||
fixesApplied,
|
||||
shouldFix,
|
||||
) {
|
||||
const ownedRootHarness = collectOwnedLegacyRootHarnessFiles(basePath);
|
||||
if (ownedRootHarness.length === 0) return;
|
||||
issues.push({
|
||||
severity: "warning",
|
||||
code: "generated_root_harness_residue",
|
||||
scope: "project",
|
||||
unitId: "project",
|
||||
message: `Found ${ownedRootHarness.length} SF-owned generated harness file(s) under root harness/. Generated operational harness belongs under .sf/harness/; promote durable contracts to docs/specs/ explicitly.`,
|
||||
file: "harness/",
|
||||
fixable: true,
|
||||
});
|
||||
if (shouldFix("generated_root_harness_residue")) {
|
||||
removeOwnedLegacyRootHarnessFiles(basePath, ownedRootHarness);
|
||||
fixesApplied.push(
|
||||
`removed ${ownedRootHarness.length} SF-owned root harness file(s)`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function parseEpochMs(value, fallbackMs) {
|
||||
if (typeof value === "number" && Number.isFinite(value)) {
|
||||
return value < 10_000_000_000 ? value * 1000 : value;
|
||||
|
|
@ -1024,6 +1099,7 @@ export async function runSFDoctor(basePath, options) {
|
|||
});
|
||||
}
|
||||
}
|
||||
checkGeneratedArtifactResidue(basePath, issues, fixesApplied, shouldFix);
|
||||
// Git health checks — timed
|
||||
const t0git = Date.now();
|
||||
const isolationMode =
|
||||
|
|
|
|||
|
|
@ -0,0 +1,91 @@
|
|||
import assert from "node:assert/strict";
|
||||
import {
|
||||
existsSync,
|
||||
mkdirSync,
|
||||
mkdtempSync,
|
||||
rmSync,
|
||||
writeFileSync,
|
||||
} from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import { dirname, join } from "node:path";
|
||||
import { afterEach, describe, test } from "vitest";
|
||||
import { runSFDoctor } from "../doctor.js";
|
||||
import { stampScaffoldFile } from "../scaffold-versioning.js";
|
||||
|
||||
const tmpDirs = [];
|
||||
|
||||
afterEach(() => {
|
||||
while (tmpDirs.length > 0) {
|
||||
const dir = tmpDirs.pop();
|
||||
if (dir) rmSync(dir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
function makeProject() {
|
||||
const dir = mkdtempSync(join(tmpdir(), "sf-doctor-generated-"));
|
||||
tmpDirs.push(dir);
|
||||
mkdirSync(join(dir, ".sf"), { recursive: true });
|
||||
return dir;
|
||||
}
|
||||
|
||||
function writeOwnedLegacyHarnessFile(root, relPath) {
|
||||
const target = join(root, relPath);
|
||||
mkdirSync(dirname(target), { recursive: true });
|
||||
writeFileSync(target, `# ${relPath}\n`, "utf-8");
|
||||
stampScaffoldFile(target, relPath, "2.75.0", "pending");
|
||||
}
|
||||
|
||||
describe("doctor generated artifact hygiene", () => {
|
||||
test("runSFDoctor_reports_sf_owned_root_harness_residue", async () => {
|
||||
const project = makeProject();
|
||||
writeOwnedLegacyHarnessFile(project, "harness/specs/bootstrap.md");
|
||||
|
||||
const report = await runSFDoctor(project, { scope: "project" });
|
||||
|
||||
const issue = report.issues.find(
|
||||
(i) => i.code === "generated_root_harness_residue",
|
||||
);
|
||||
assert.equal(issue?.fixable, true);
|
||||
assert.equal(existsSync(join(project, "harness/specs/bootstrap.md")), true);
|
||||
});
|
||||
|
||||
test("runSFDoctor_fix_removes_only_sf_owned_pending_root_harness", async () => {
|
||||
const project = makeProject();
|
||||
writeOwnedLegacyHarnessFile(project, "harness/specs/bootstrap.md");
|
||||
|
||||
const report = await runSFDoctor(project, {
|
||||
fix: true,
|
||||
fixLevel: "all",
|
||||
scope: "project",
|
||||
});
|
||||
|
||||
assert.ok(
|
||||
report.fixesApplied.includes("removed 1 SF-owned root harness file(s)"),
|
||||
);
|
||||
assert.equal(existsSync(join(project, "harness")), false);
|
||||
});
|
||||
|
||||
test("runSFDoctor_ignores_user_owned_root_harness", async () => {
|
||||
const project = makeProject();
|
||||
mkdirSync(join(project, "harness/specs"), { recursive: true });
|
||||
writeFileSync(
|
||||
join(project, "harness/specs/project-contract.md"),
|
||||
"# Project-owned harness\n",
|
||||
);
|
||||
|
||||
const report = await runSFDoctor(project, {
|
||||
fix: true,
|
||||
fixLevel: "all",
|
||||
scope: "project",
|
||||
});
|
||||
|
||||
assert.equal(
|
||||
report.issues.some((i) => i.code === "generated_root_harness_residue"),
|
||||
false,
|
||||
);
|
||||
assert.equal(
|
||||
existsSync(join(project, "harness/specs/project-contract.md")),
|
||||
true,
|
||||
);
|
||||
});
|
||||
});
|
||||
Loading…
Add table
Reference in a new issue