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:
gsxdsm
2026-05-30 14:07:20 -07:00
parent 90a23e9fc1
commit 9c4e8edf39
14 changed files with 357 additions and 11 deletions

View File

@@ -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);