fix(missions): address generated fix review blockers

This commit is contained in:
Phil Larson
2026-07-05 22:14:22 -07:00
parent 3744fbcc2f
commit 3d60f67fab
4 changed files with 83 additions and 14 deletions

View File

@@ -1699,17 +1699,25 @@ describe("MissionStore", () => {
store.completeValidatorRun(fix1FailRun.id, "failed", "still missing evidence");
const fix2 = store.createGeneratedFixFeature(fix1.id, fix1FailRun.id, ["CA-fix1"]);
createTaskInDb(db, "FN-fix1", "Stale fix task", undefined, { column: "todo" });
store.updateFeature(fix1.id, { taskId: "FN-fix1" });
db.prepare("UPDATE tasks SET missionId = ?, sliceId = ? WHERE id = ?").run(mission.id, slice.id, "FN-fix1");
store.updateFeature(fix1.id, { status: "blocked", loopState: "blocked", lastValidatorStatus: "failed" });
store.updateFeature(fix2.id, { status: "blocked", loopState: "blocked", lastValidatorStatus: "error" });
store.updateFeature(source.id, { status: "done", loopState: "passed", lastValidatorStatus: "passed" });
expect(store.computeSliceStatus(slice.id)).toBe("pending");
expect(store.computeSliceStatus(slice.id)).not.toBe("complete");
const report = store.reconcileSupersededGeneratedFixFeatures(slice.id);
expect(report).toEqual({ supersededCount: 2, featureIds: [fix1.id, fix2.id] });
expect(store.getFeature(fix1.id)).toMatchObject({ status: "done", loopState: "passed", lastValidatorStatus: "passed" });
expect(store.getFeature(fix1.id)).toMatchObject({ status: "done", taskId: undefined, loopState: "passed", lastValidatorStatus: "passed" });
expect(store.getFeature(fix2.id)).toMatchObject({ status: "done", loopState: "passed", lastValidatorStatus: "passed" });
expect(db.prepare("SELECT missionId, sliceId FROM tasks WHERE id = ?").get("FN-fix1")).toEqual({
missionId: null,
sliceId: null,
});
expect(store.computeSliceStatus(slice.id)).toBe("complete");
});
@@ -1727,9 +1735,16 @@ describe("MissionStore", () => {
expect(store.computeSliceStatus(slice.id)).toBe("pending");
const events: string[] = [];
store.on("validator-run:completed", () => events.push("validator-run:completed"));
store.on("feature:updated", (feature: MissionFeature) => {
if (feature.id === staleFix.id) events.push("stale-fix:updated");
});
const passingRun = store.startValidatorRun(source.id, "task_completion");
store.completeValidatorRun(passingRun.id, "passed", "evidence now complete");
expect(events).toEqual(["validator-run:completed", "stale-fix:updated"]);
expect(store.getFeature(staleFix.id)).toMatchObject({ status: "done", loopState: "passed", lastValidatorStatus: "passed" });
expect(store.computeSliceStatus(slice.id)).toBe("complete");
});

View File

@@ -2750,6 +2750,7 @@ export class MissionStore extends EventEmitter<MissionStoreEvents> {
// Re-read the run to get the updated state
const updatedRun = this.getValidatorRun(runId)!;
this.emit("validator-run:completed", updatedRun, result, durationMs);
if (result === "passed") {
const passedFeature = this.getFeature(run.featureId);
@@ -2758,8 +2759,6 @@ export class MissionStore extends EventEmitter<MissionStoreEvents> {
}
}
this.emit("validator-run:completed", updatedRun, result, durationMs);
return updatedRun;
}
@@ -3178,6 +3177,10 @@ export class MissionStore extends EventEmitter<MissionStoreEvents> {
* feature is later validated successfully, older descendants are no longer
* actionable remediation work. Leaving them blocked/defined keeps the slice
* pending forever even though the authoritative source feature has passed.
*
* FNXC:Missions 2026-07-05-22:09:
* Superseded generated Fix Features must become terminal and lose live board-task ownership.
* Otherwise mission recovery can keep resuming stale remediation tasks after the source feature is already validated.
*/
reconcileSupersededGeneratedFixFeatures(sliceId: string): { supersededCount: number; featureIds: string[] } {
const features = this.listFeatures(sliceId);
@@ -3211,17 +3214,22 @@ export class MissionStore extends EventEmitter<MissionStoreEvents> {
.filter((feature) => feature.status !== "done" || feature.loopState !== "passed" || feature.lastValidatorStatus !== "passed")
.map((feature) => feature.id);
for (const featureId of supersededFeatureIds) {
this.updateFeature(featureId, {
status: "done",
loopState: "passed",
lastValidatorStatus: "passed",
});
}
if (supersededFeatureIds.length > 0) {
this.recomputeSliceStatus(sliceId);
this.db.bumpLastModified();
this.db.transaction(() => {
for (const featureId of supersededFeatureIds) {
const feature = this.getFeature(featureId);
if (!feature) continue;
this.updateFeature(featureId, {
status: "done",
taskId: undefined,
loopState: "passed",
lastValidatorStatus: "passed",
});
if (feature.taskId) {
this.db.prepare("UPDATE tasks SET missionId = NULL, sliceId = NULL WHERE id = ? AND \"deletedAt\" IS NULL").run(feature.taskId);
}
}
});
}
return {

View File

@@ -663,6 +663,47 @@ describe("MissionExecutionLoop", () => {
expect(missionStore.startValidatorRun).not.toHaveBeenCalled();
});
it("skips superseded generated fixes from the stale recovery snapshot", async () => {
const mission = createMockMission({ id: "M-SUPERSEDE", status: "active" });
missionStore._clear();
missionStore._setMission(mission);
const slice = createMockSlice({ id: "SL-SUPERSEDE", milestoneId: "MS-001", status: "active" });
const source = createMockFeature({
id: "F-SOURCE",
sliceId: slice.id,
status: "done",
loopState: "passed",
lastValidatorStatus: "passed",
});
const staleFix = createMockFeature({
id: "F-STALE-FIX",
sliceId: slice.id,
status: "defined",
loopState: "validating",
generatedFromFeatureId: source.id,
taskId: "FN-stale-fix",
});
missionStore._setFeature(source);
missionStore._setFeature(staleFix);
taskStore._setTask({ id: "FN-stale-fix", column: "done" });
wireHierarchy(slice, [source, staleFix]);
loop = new MissionExecutionLoop({
taskStore: taskStore as any,
missionStore: missionStore as any,
rootDir: "/tmp",
});
loop.start();
const result = await loop.recoverActiveMissions();
expect(missionStore.reconcileSupersededGeneratedFixFeatures).toHaveBeenCalledWith(slice.id);
expect(missionStore.transitionLoopState).not.toHaveBeenCalledWith(staleFix.id, "implementing");
expect(taskStore.getTask).not.toHaveBeenCalledWith("FN-stale-fix");
expect(result).toEqual({ recoveredCount: 1 });
});
});
describe("reapStaleValidatorRuns", () => {

View File

@@ -250,6 +250,7 @@ export class MissionExecutionLoop extends EventEmitter {
if (slice.status !== "active") continue;
const supersededFixes = this.missionStore.reconcileSupersededGeneratedFixFeatures(slice.id);
const supersededFeatureIds = new Set(supersededFixes.featureIds);
if (supersededFixes.supersededCount > 0) {
loopLog.warn(
`Recovery: superseded ${supersededFixes.supersededCount} generated Fix Features in slice ${slice.id} `
@@ -259,6 +260,10 @@ export class MissionExecutionLoop extends EventEmitter {
}
for (const feature of slice.features) {
if (supersededFeatureIds.has(feature.id)) {
continue;
}
// Features in validating state need to be re-validated
if (feature.loopState === "validating") {
loopLog.log(`Recovery: re-queuing validating feature ${feature.id}`);