sf snapshot: uncommitted changes after 32m inactivity

This commit is contained in:
Mikael Hugo 2026-05-08 18:51:07 +02:00
parent 7318af029a
commit 533d1ce83c

View file

@ -114,7 +114,7 @@ function openRawDb(path) {
loadProvider();
return new DatabaseSync(path);
}
const SCHEMA_VERSION = 45;
const SCHEMA_VERSION = 46;
function indexExists(db, name) {
return !!db
.prepare(
@ -2541,6 +2541,55 @@ function migrateSchema(db) {
":applied_at": new Date().toISOString(),
});
}
if (currentVersion < 46) {
// validation_runs: mirrors droid's validation-contract.md + validation-state.json
// pattern. Each run stores the contract spec inline and its execution state.
db.exec(`
CREATE TABLE IF NOT EXISTS validation_runs (
run_id TEXT PRIMARY KEY,
milestone_id TEXT NOT NULL,
slice_id TEXT,
task_id TEXT,
contract TEXT NOT NULL DEFAULT '',
status TEXT NOT NULL DEFAULT 'pending',
verdict TEXT NOT NULL DEFAULT '',
rationale TEXT NOT NULL DEFAULT '',
findings TEXT NOT NULL DEFAULT '',
started_at TEXT,
completed_at TEXT,
created_at TEXT NOT NULL DEFAULT (datetime('now')),
superseded_by TEXT,
FOREIGN KEY (milestone_id) REFERENCES milestones(id)
)
`);
db.exec(`
CREATE INDEX IF NOT EXISTS idx_validation_runs_scope
ON validation_runs(milestone_id, slice_id, task_id)
`);
db.exec(`
CREATE VIEW IF NOT EXISTS latest_validation_state AS
SELECT vr.*
FROM validation_runs vr
INNER JOIN (
SELECT milestone_id,
COALESCE(slice_id, '') AS slice_id,
COALESCE(task_id, '') AS task_id,
MAX(created_at) AS max_created
FROM validation_runs
GROUP BY milestone_id, slice_id, task_id
) latest
ON vr.milestone_id = latest.milestone_id
AND COALESCE(vr.slice_id, '') = latest.slice_id
AND COALESCE(vr.task_id, '') = latest.task_id
AND vr.created_at = latest.max_created
`);
db.prepare(
"INSERT INTO schema_version (version, applied_at) VALUES (:version, :applied_at)",
).run({
":version": 46,
":applied_at": new Date().toISOString(),
});
}
db.exec("COMMIT");
} catch (err) {
db.exec("ROLLBACK");
@ -7215,3 +7264,116 @@ export function getTaskSpec(milestoneId, sliceId, taskId) {
)
.get(milestoneId, sliceId, taskId);
}
// ─── Validation Runs ───────────────────────────────────────────────────────────
/**
* Start a validation run for a milestone, slice, or task.
* Mirrors droid's validation-state.json creation from validation-contract.md.
*
* Purpose: Track explicit validation contracts and their execution state in the
* DB so any surface (CLI, TUI, headless) can answer "what are we validating and
* where are we" with a single query.
*
* Consumer: autonomous-solver, plan-slice, quality gates, eval runners.
*/
export function startValidationRun({ milestoneId, sliceId, taskId, contract }) {
if (!currentDb) throw new SFError(SF_STALE_STATE, "sf-db: No database open");
const runId = crypto.randomUUID();
currentDb
.prepare(
`INSERT INTO validation_runs
(run_id, milestone_id, slice_id, task_id, contract, status, started_at)
VALUES (:run_id, :milestone_id, :slice_id, :task_id, :contract, 'running', datetime('now'))`,
)
.run({
":run_id": runId,
":milestone_id": milestoneId,
":slice_id": sliceId ?? null,
":task_id": taskId ?? null,
":contract": contract ?? "",
});
return runId;
}
/**
* Complete a validation run with verdict and findings.
* Mirrors droid's update of validation-state.json after run finishes.
*
* Consumer: autonomous-solver after eval execution, quality gate evaluators.
*/
export function completeValidationRun({
runId,
verdict,
rationale = "",
findings = "",
}) {
if (!currentDb) throw new SFError(SF_STALE_STATE, "sf-db: No database open");
currentDb
.prepare(
`UPDATE validation_runs SET
status = :status,
verdict = :verdict,
rationale = :rationale,
findings = :findings,
completed_at = datetime('now')
WHERE run_id = :run_id`,
)
.run({
":run_id": runId,
":status": verdict === "pass" ? "pass" : verdict === "fail" ? "fail" : "error",
":verdict": verdict ?? "",
":rationale": rationale ?? "",
":findings": findings ?? "",
});
}
/**
* Get the latest validation state for a scope (milestone, slice, or task).
* Returns the most recent run mirrors droid's validation-state.json read.
*
* Consumer: any surface that needs "are we passing?" for a milestone/slice/task.
*/
export function getLatestValidationState(milestoneId, sliceId, taskId) {
if (!currentDb) return null;
const rows = currentDb
.prepare(
`SELECT * FROM validation_runs
WHERE milestone_id = :milestone_id
AND (:slice_id IS NULL OR slice_id = :slice_id)
AND (:task_id IS NULL OR task_id = :task_id)
ORDER BY created_at DESC
LIMIT 1`,
)
.all({
":milestone_id": milestoneId,
":slice_id": sliceId ?? null,
":task_id": taskId ?? null,
});
return rows[0] ?? null;
}
/**
* Get validation run history for a scope.
* Mirrors droid's historical validation-state.json files.
*
* Consumer: forensics, eval review, audit trail queries.
*/
export function getValidationHistory(milestoneId, sliceId, taskId, limit = 20) {
if (!currentDb) return [];
return currentDb
.prepare(
`SELECT * FROM validation_runs
WHERE milestone_id = :milestone_id
AND (:slice_id IS NULL OR slice_id = :slice_id)
AND (:task_id IS NULL OR task_id = :task_id)
ORDER BY created_at DESC
LIMIT :limit`,
)
.all({
":milestone_id": milestoneId,
":slice_id": sliceId ?? null,
":task_id": taskId ?? null,
":limit": limit,
});
}