fix: guard completeValidationRun and drop dead superseded_by column

- completeValidationRun now checks status='running' in WHERE clause and
  throws if no row was updated (catches double-complete and invalid runId)
- Remove unused superseded_by column from v46 CREATE TABLE DDL
- Add migration v47 to DROP COLUMN superseded_by from existing DBs
- Bump SCHEMA_VERSION to 47

Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
This commit is contained in:
Mikael Hugo 2026-05-08 21:45:58 +02:00
parent 6e6363da0d
commit c5e9e4f9c8

View file

@ -114,7 +114,7 @@ function openRawDb(path) {
loadProvider();
return new DatabaseSync(path);
}
const SCHEMA_VERSION = 46;
const SCHEMA_VERSION = 47;
function indexExists(db, name) {
return !!db
.prepare(
@ -2558,7 +2558,6 @@ function migrateSchema(db) {
started_at TEXT,
completed_at TEXT,
created_at TEXT NOT NULL,
superseded_by TEXT,
FOREIGN KEY (milestone_id) REFERENCES milestones(id)
)
`);
@ -2585,6 +2584,25 @@ function migrateSchema(db) {
":applied_at": new Date().toISOString(),
});
}
if (currentVersion < 47) {
// Drop unused superseded_by column from validation_runs.
// The column was never written or queried — dead schema from v46.
const cols = db
.prepare("PRAGMA table_info(validation_runs)")
.all()
.map((c) => c.name);
if (cols.includes("superseded_by")) {
db.exec(
"ALTER TABLE validation_runs DROP COLUMN superseded_by",
);
}
db.prepare(
"INSERT INTO schema_version (version, applied_at) VALUES (:version, :applied_at)",
).run({
":version": 47,
":applied_at": new Date().toISOString(),
});
}
db.exec("COMMIT");
} catch (err) {
db.exec("ROLLBACK");
@ -7304,7 +7322,9 @@ export function completeValidationRun({
findings = "",
}) {
if (!currentDb) throw new SFError(SF_STALE_STATE, "sf-db: No database open");
currentDb
const status =
verdict === "pass" ? "pass" : verdict === "fail" ? "fail" : "error";
const result = currentDb
.prepare(
`UPDATE validation_runs SET
status = :status,
@ -7312,15 +7332,21 @@ export function completeValidationRun({
rationale = :rationale,
findings = :findings,
completed_at = datetime('now')
WHERE run_id = :run_id`,
WHERE run_id = :run_id AND status = 'running'`,
)
.run({
":run_id": runId,
":status": verdict === "pass" ? "pass" : verdict === "fail" ? "fail" : "error",
":status": status,
":verdict": verdict ?? "",
":rationale": rationale ?? "",
":findings": findings ?? "",
});
if (result.changes === 0) {
throw new SFError(
SF_STALE_STATE,
`sf-db: completeValidationRun: no running validation run found for run_id=${runId}`,
);
}
}
/**