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:
@@ -7249,6 +7249,7 @@ export interface MilestoneValidationRollup {
|
||||
blockedAssertions: number;
|
||||
pendingAssertions: number;
|
||||
unlinkedAssertions: number;
|
||||
hasProseButNoAssertions: boolean;
|
||||
state: "not_started" | "needs_coverage" | "ready" | "passed" | "failed" | "blocked";
|
||||
}
|
||||
|
||||
|
||||
@@ -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 */
|
||||
|
||||
@@ -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
|
||||
)
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -133,6 +133,7 @@ export interface MilestoneValidationRollup {
|
||||
blockedAssertions: number;
|
||||
pendingAssertions: number;
|
||||
unlinkedAssertions: number;
|
||||
hasProseButNoAssertions: boolean;
|
||||
state: MilestoneValidationState;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user