FN-5733: enforce mission completion-gate assertions for live Goals mission
Realize the mission completion-gate contract by surfacing and enforcing structured assertion coverage for milestone completion. - add milestone rollup signaling for prose-only acceptance criteria and wire it into mission-store completion gating - update mission execution loop reliability paths so validator-trigger continuity is preserved across completion and startup recovery - expose the new rollup flag in dashboard mission types/API and show an autopilot warning badge in MissionManager when prose exists without structured assertions - extend core, engine, and dashboard tests plus mission docs, and add a patch changeset for @runfusion/fusion Files changed: .changeset/fn-5733-mission-completion-gate.md | 10 +++ docs/missions-completion-contract.md | 11 +++ docs/missions.md | 4 + packages/core/src/__tests__/mission-store.test.ts | 81 ++++++++++++++++++++ packages/core/src/mission-store.ts | 89 ++++++++++++++++++++++ packages/core/src/mission-types.ts | 2 + packages/dashboard/app/api/legacy.ts | 1 + .../dashboard/app/components/MissionManager.css | 42 +++++++++- .../dashboard/app/components/MissionManager.tsx | 22 +++++- .../components/__tests__/MissionManager.test.tsx | 20 ++++- packages/dashboard/app/components/mission-types.ts | 1 + .../src/__tests__/mission-execution-loop.test.ts | 23 ++++++ .../mission-validation-trigger-gap.test.ts | 41 ++++++++++ packages/engine/src/mission-execution-loop.ts | 21 ++++- 14 files changed, 357 insertions(+), 11 deletions(-) Fusion-Task-Id: FN-5733 Fusion-Task-Lineage: 259418c6-5404-4773-8fd2-db8d7e9cadd3
This commit is contained in:
@@ -3032,6 +3032,49 @@ describe("MissionStore", () => {
|
||||
expect(events[events.length - 1].state).toBe("ready"); // linked but not passed
|
||||
expect(events[events.length - 1].rollup.unlinkedAssertions).toBe(0);
|
||||
});
|
||||
|
||||
it("flags rollup when milestone prose exists but no assertions are linked", () => {
|
||||
const updatedMission = store.updateMission(mission.id, { status: "active" });
|
||||
expect(updatedMission.status).toBe("active");
|
||||
store.updateMilestone(milestone.id, { acceptanceCriteria: "Milestone prose" });
|
||||
|
||||
const warningEvents: Array<{ id: string; code: unknown }> = [];
|
||||
store.on("mission:event", (event) => {
|
||||
if (event.eventType === "warning") {
|
||||
warningEvents.push({ id: event.id, code: event.metadata?.code });
|
||||
}
|
||||
});
|
||||
|
||||
const rollup = store.getMilestoneValidationRollup(milestone.id);
|
||||
expect(rollup.hasProseButNoAssertions).toBe(true);
|
||||
expect(store.milestoneHasProseButNoAssertions(milestone.id)).toBe(true);
|
||||
|
||||
const assertion = store.addContractAssertion(milestone.id, { title: "A1", assertion: "Temp" });
|
||||
store.deleteContractAssertion(assertion.id);
|
||||
|
||||
expect(warningEvents.some((event) => event.code === "milestone_missing_structured_assertions")).toBe(true);
|
||||
});
|
||||
|
||||
it("does not flag rollup when assertions exist", () => {
|
||||
store.updateMilestone(milestone.id, { acceptanceCriteria: "Milestone prose" });
|
||||
const assertion = store.addContractAssertion(milestone.id, { title: "A1", assertion: "Test" });
|
||||
const slice = store.addSlice(milestone.id, { title: "Slice" });
|
||||
const feature = store.addFeature(slice.id, { title: "Feature" });
|
||||
store.linkFeatureToAssertion(feature.id, assertion.id);
|
||||
|
||||
const rollup = store.getMilestoneValidationRollup(milestone.id);
|
||||
expect(rollup.hasProseButNoAssertions).toBe(false);
|
||||
expect(store.milestoneHasProseButNoAssertions(milestone.id)).toBe(false);
|
||||
});
|
||||
|
||||
it("does not flag rollup when neither milestone nor features have prose", () => {
|
||||
const slice = store.addSlice(milestone.id, { title: "Slice" });
|
||||
store.addFeature(slice.id, { title: "Feature" });
|
||||
|
||||
const rollup = store.getMilestoneValidationRollup(milestone.id);
|
||||
expect(rollup.hasProseButNoAssertions).toBe(false);
|
||||
expect(store.milestoneHasProseButNoAssertions(milestone.id)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
// ── buildEnrichedDescription with Assertions Tests ────────────────────
|
||||
@@ -3149,6 +3192,44 @@ describe("MissionStore", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("seedContractAssertionsForFeatures", () => {
|
||||
it("seeds and links authored assertions idempotently", () => {
|
||||
const mission = store.createMission({ title: "Seed mission" });
|
||||
const milestone = store.addMilestone(mission.id, { title: "M1" });
|
||||
const slice = store.addSlice(milestone.id, { title: "S1" });
|
||||
const feature = store.addFeature(slice.id, { title: "F1", acceptanceCriteria: "AC" });
|
||||
|
||||
const beforeManaged = store.listAssertionsForFeature(feature.id).length;
|
||||
|
||||
const first = store.seedContractAssertionsForFeatures([
|
||||
{
|
||||
featureId: feature.id,
|
||||
milestoneId: milestone.id,
|
||||
title: "Authored assertion",
|
||||
assertion: "Feature output is deterministic",
|
||||
},
|
||||
]);
|
||||
|
||||
expect(first.created).toBe(1);
|
||||
expect(first.linked).toBe(1);
|
||||
expect(first.skippedExisting).toBe(0);
|
||||
|
||||
const second = store.seedContractAssertionsForFeatures([
|
||||
{
|
||||
featureId: feature.id,
|
||||
milestoneId: milestone.id,
|
||||
title: "Authored assertion",
|
||||
assertion: "Feature output is deterministic",
|
||||
},
|
||||
]);
|
||||
|
||||
expect(second.created).toBe(0);
|
||||
expect(second.linked).toBe(0);
|
||||
expect(second.skippedExisting).toBe(1);
|
||||
expect(store.listAssertionsForFeature(feature.id).length).toBe(beforeManaged + 1);
|
||||
});
|
||||
});
|
||||
|
||||
describe("backfillFeatureAssertions", () => {
|
||||
const makeLegacyFeature = (sliceId: string, input: { title: string; description?: string; acceptanceCriteria?: string }) => {
|
||||
const feature = store.addFeature(sliceId, input);
|
||||
|
||||
Reference in New Issue
Block a user