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

@@ -7249,6 +7249,7 @@ export interface MilestoneValidationRollup {
blockedAssertions: number;
pendingAssertions: number;
unlinkedAssertions: number;
hasProseButNoAssertions: boolean;
state: "not_started" | "needs_coverage" | "ready" | "passed" | "failed" | "blocked";
}

View File

@@ -1708,14 +1708,44 @@
align-items: center;
gap: var(--space-sm);
margin-bottom: var(--space-sm);
flex-wrap: wrap;
}
.mission-assertions__title {
font-size: 13px;
font-size: calc(var(--space-sm) + var(--space-xs) * 0.75);
font-weight: 600;
color: var(--text-muted);
text-transform: uppercase;
letter-spacing: 0.5px;
letter-spacing: calc(var(--btn-border-width) * 2);
}
.mission-assertions__mode-tag {
display: inline-flex;
align-items: center;
gap: var(--space-xs);
font-size: calc(var(--space-sm) + var(--space-xs) * 0.5);
color: var(--text-dim);
background: color-mix(in srgb, var(--surface) 85%, var(--bg));
border: 1px solid var(--border);
border-radius: var(--radius-sm);
padding: calc(var(--space-xs) * 0.5) var(--space-sm);
}
.mission-assertions__mode-tag--warning {
color: var(--color-warning);
border-color: color-mix(in srgb, var(--color-warning) 40%, var(--border));
}
.mission-assertions__mode-tag--informational {
color: var(--text-muted);
}
.mission-assertions__rollup-header {
display: flex;
align-items: center;
justify-content: space-between;
gap: var(--space-sm);
flex-wrap: wrap;
}
.mission-assertions__list {
@@ -2742,8 +2772,12 @@
/* Assertions coverage bar responsive */
.mission-assertions__coverage-bar {
min-width: 30px;
max-width: 60px;
min-width: calc(var(--space-lg) * 2);
max-width: calc(var(--space-lg) * 4);
}
.mission-assertions__rollup-header {
align-items: flex-start;
}
/* Feature lineage and retry budget wrap on narrow screens */

View File

@@ -3486,7 +3486,17 @@ export function MissionManager({ isOpen, isInline = false, onClose, addToast, pr
{/* Assertions Panel */}
<div className="mission-assertions">
<div className="mission-assertions__header">
<span className="mission-assertions__title">Assertions</span>
<span className="mission-assertions__title">Contract assertions (autopilot gate)</span>
<span className="mission-assertions__mode-tag" data-testid="milestone-assertions-enforced-indicator">
<span className="status-dot status-dot--running" />
Enforced by autopilot
</span>
{milestoneRollup?.hasProseButNoAssertions && (
<span className="mission-assertions__mode-tag mission-assertions__mode-tag--warning" data-testid="milestone-missing-structured-assertions-badge">
<span className="status-dot status-dot--pending" />
Prose criteria found; add contract assertions
</span>
)}
{milestoneRollup && (
<span
className="mission-status-badge mission-status-badge--sm"
@@ -3741,7 +3751,13 @@ export function MissionManager({ isOpen, isInline = false, onClose, addToast, pr
// even when milestone.acceptanceCriteria is also populated (FN-4613/FN-4652).
// If FN-4578/4579/4580 (or successors) change the model, update this.
<div className="mission-assertions__list" data-testid="milestone-feature-acceptance-rollup">
<span className="mission-assertions__title">Completion criteria (from features)</span>
<div className="mission-assertions__rollup-header">
<span className="mission-assertions__title">Feature acceptance criteria (informational)</span>
<span className="mission-assertions__mode-tag mission-assertions__mode-tag--informational" data-testid="milestone-feature-acceptance-informational-indicator">
<span className="status-dot status-dot--pending" />
Not enforced by autopilot
</span>
</div>
{featuresWithAcceptanceCriteria.map((feature) => (
<div key={feature.id} className="mission-assertion">
<span className="mission-assertion__title">{feature.title}</span>
@@ -3755,7 +3771,7 @@ export function MissionManager({ isOpen, isInline = false, onClose, addToast, pr
) : (
!milestone.acceptanceCriteria?.trim() ? (
<div className="mission-manager__empty mission-assertions__empty">
<span>No assertions defined. Add one to define completion criteria.</span>
<span>No contract assertions defined yet. Feature acceptance criteria are informational until assertions are added.</span>
</div>
) : null
)

View File

@@ -173,6 +173,7 @@ const mockMilestoneValidationRollup = {
blockedAssertions: 0,
pendingAssertions: 0,
unlinkedAssertions: 0,
hasProseButNoAssertions: false,
state: "not_started" as const,
};
@@ -236,6 +237,7 @@ const mockMilestoneValidationTelemetryWithRounds = {
blockedAssertions: 0,
pendingAssertions: 0,
unlinkedAssertions: 0,
hasProseButNoAssertions: false,
state: "failed" as const,
},
};
@@ -276,6 +278,7 @@ const mockBlockedMilestoneTelemetry = {
blockedAssertions: 1,
pendingAssertions: 0,
unlinkedAssertions: 0,
hasProseButNoAssertions: false,
state: "blocked" as const,
},
};
@@ -4635,7 +4638,7 @@ describe("MissionManager", () => {
});
describe("milestone assertions empty-state", () => {
const emptyAssertionsCopy = "No assertions defined. Add one to define completion criteria.";
const emptyAssertionsCopy = "No contract assertions defined yet. Feature acceptance criteria are informational until assertions are added.";
it("keeps empty-state nudge when assertions and feature acceptance criteria are both missing", async () => {
const missionDetail = JSON.parse(JSON.stringify(mockMissionDetail)) as typeof mockMissionDetail;
@@ -4698,7 +4701,15 @@ describe("MissionManager", () => {
},
];
globalThis.fetch = createDetailFetchMockForMissionDetail(missionDetail);
const telemetryOverride = {
...mockMilestoneValidationTelemetry,
rollup: {
...mockMilestoneValidationRollup,
hasProseButNoAssertions: true,
},
};
globalThis.fetch = createDetailFetchMockForMissionDetail(missionDetail, telemetryOverride);
render(<MissionManager isOpen={true} onClose={vi.fn()} addToast={vi.fn()} />);
fireEvent.click(await screen.findByText("Build Auth System"));
@@ -4706,11 +4717,13 @@ describe("MissionManager", () => {
expect(screen.queryByText(emptyAssertionsCopy)).not.toBeInTheDocument();
const rollup = screen.getByTestId("milestone-feature-acceptance-rollup");
expect(within(rollup).getByText("Completion criteria (from features)")).toBeInTheDocument();
expect(within(rollup).getByText("Feature acceptance criteria (informational)")).toBeInTheDocument();
expect(within(rollup).getByTestId("milestone-feature-acceptance-informational-indicator")).toHaveTextContent("Not enforced by autopilot");
expect(within(rollup).getByText("Session handling")).toBeInTheDocument();
expect(within(rollup).getByText("Session refresh succeeds without logout", { exact: false })).toBeInTheDocument();
expect(within(rollup).getByText("Token storage")).toBeInTheDocument();
expect(within(rollup).getByText("Tokens remain encrypted at rest", { exact: false })).toBeInTheDocument();
expect(screen.getByTestId("milestone-missing-structured-assertions-badge")).toHaveTextContent("Prose criteria found; add contract assertions");
});
it("keeps structured assertions precedence and hides rollup when assertions exist", async () => {
@@ -4725,6 +4738,7 @@ describe("MissionManager", () => {
await waitForDetailLoaded();
expect(screen.getByText("Auth works")).toBeInTheDocument();
expect(screen.getByTestId("milestone-assertions-enforced-indicator")).toHaveTextContent("Enforced by autopilot");
expect(screen.queryByTestId("milestone-feature-acceptance-rollup")).not.toBeInTheDocument();
});
});

View File

@@ -133,6 +133,7 @@ export interface MilestoneValidationRollup {
blockedAssertions: number;
pendingAssertions: number;
unlinkedAssertions: number;
hasProseButNoAssertions: boolean;
state: MilestoneValidationState;
}