FN-8387: add canonical lineage approval predicate

Centralize symbol-lock admission rules for resolved mission lineage.

- Add a pure core predicate with stable approval and rejection reasons.
- Export the lineage contract and cover status and plan-fingerprint cases.
- Document scheduler ownership and the admission requirements.

Files changed:
 docs/missions.md                                   |  15 +++
 .../__tests__/symbol-lock-lineage-approval.test.ts | 123 +++++++++++++++++++++
 packages/core/src/index.ts                         |  10 ++
 packages/core/src/symbol-lock-lineage-approval.ts  |  87 +++++++++++++++
 4 files changed, 235 insertions(+)

Fusion-Task-Id: FN-8387

Fusion-Task-Lineage: dc2d8518-ffff-4062-b11f-a162dd1b10da

Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
This commit is contained in:
gsxdsm
2026-07-19 15:20:39 -07:00
parent bea51f7bdb
commit 2c72ac0c0c
4 changed files with 235 additions and 0 deletions

View File

@@ -23,6 +23,21 @@ Mission: Improve Reliability
Task: FN-214
```
## Canonical lineage approval for autonomous symbol locks
Before autonomous scheduler work may acquire a symbol lock, it resolves the task's Mission → Milestone → Slice → Feature lineage and evaluates the single `@fusion/core` contract: `evaluateMissionLineageApproval`. Resolution and lock acquisition remain scheduler responsibilities; downstream schedulers must not redefine the approval rule.
Approval requires every one of these statuses:
- Mission: `active`
- Milestone: `active`
- Slice: `active`
- Feature: `triaged` or `in-progress`
When the scheduler passes `planApprovalRequired: true`, the linked task must also have an `approvedPlanFingerprint` that is a non-empty string after trimming whitespace. The predicate does not recompute the fingerprint; `plan-approval.ts` owns its generation and validation. When plan approval is not required, the fingerprint is ignored.
The predicate is pure and returns `{ approved, reason }`. Its stable reasons are `approved`, `missing-mission`, `missing-milestone`, `missing-slice`, `missing-feature`, `mission-not-active`, `milestone-not-active`, `slice-not-active`, `feature-not-implementable`, and `plan-not-approved`. A false result is the scheduler's `lineage-blocked` outcome; only an approved result is eligible for symbol-lock admission.
## Mission → Goal linkage
Missions and goals are stored independently, with an optional many-to-many linkage persisted in the `mission_goals` join table.

View File

@@ -0,0 +1,123 @@
import { describe, expect, it } from "vitest";
import {
evaluateMissionLineageApproval,
isMissionLineageApproved,
MISSION_LINEAGE_APPROVAL_REQUIRED,
type MissionLineageSnapshot,
} from "../index.js";
import type { Milestone, Mission, MissionFeature, Slice } from "../mission-types.js";
const timestamp = "2026-07-19T14:58:00.000Z";
function mission(status: Mission["status"] = "active"): Mission {
return { id: "M-1", title: "Mission", status, interviewState: "completed", createdAt: timestamp, updatedAt: timestamp };
}
function milestone(status: Milestone["status"] = "active"): Milestone {
return {
id: "MS-1", missionId: "M-1", title: "Milestone", status, orderIndex: 0,
interviewState: "completed", dependencies: [], createdAt: timestamp, updatedAt: timestamp,
};
}
function slice(status: Slice["status"] = "active"): Slice {
return {
id: "SL-1", milestoneId: "MS-1", title: "Slice", status, orderIndex: 0,
planState: "planned", createdAt: timestamp, updatedAt: timestamp,
};
}
function feature(status: MissionFeature["status"] = "triaged"): MissionFeature {
return { id: "F-1", sliceId: "SL-1", title: "Feature", status, createdAt: timestamp, updatedAt: timestamp };
}
function snapshot(overrides: Partial<MissionLineageSnapshot> = {}): MissionLineageSnapshot {
return {
mission: mission(),
milestone: milestone(),
slice: slice(),
feature: feature(),
task: {},
planApprovalRequired: false,
...overrides,
};
}
describe("evaluateMissionLineageApproval", () => {
it.each([
["mission", "missing-mission"],
["milestone", "missing-milestone"],
["slice", "missing-slice"],
["feature", "missing-feature"],
] as const)("reports %s absence before later requirements", (link, reason) => {
expect(evaluateMissionLineageApproval(snapshot({ [link]: undefined }))).toEqual({ approved: false, reason });
});
it.each(["planning", "blocked", "complete", "archived"] as const)("rejects %s missions", (status) => {
expect(evaluateMissionLineageApproval(snapshot({ mission: mission(status) }))).toEqual({
approved: false, reason: "mission-not-active",
});
});
it.each(["planning", "blocked", "complete"] as const)("rejects %s milestones", (status) => {
expect(evaluateMissionLineageApproval(snapshot({ milestone: milestone(status) }))).toEqual({
approved: false, reason: "milestone-not-active",
});
});
it.each(["pending", "complete"] as const)("rejects %s slices", (status) => {
expect(evaluateMissionLineageApproval(snapshot({ slice: slice(status) }))).toEqual({
approved: false, reason: "slice-not-active",
});
});
it.each(["defined", "done", "blocked"] as const)("rejects %s features", (status) => {
expect(evaluateMissionLineageApproval(snapshot({ feature: feature(status) }))).toEqual({
approved: false, reason: "feature-not-implementable",
});
});
it.each(["triaged", "in-progress"] as const)("accepts implementable %s features without plan approval", (status) => {
const input = snapshot({ feature: feature(status) });
expect(evaluateMissionLineageApproval(input)).toEqual({ approved: true, reason: "approved" });
expect(isMissionLineageApproved(input)).toBe(true);
});
it("ignores fingerprints when plan approval is not required", () => {
expect(evaluateMissionLineageApproval(snapshot({ task: { approvedPlanFingerprint: " " } }))).toEqual({
approved: true, reason: "approved",
});
});
it.each([undefined, "", " \t\n "])("requires a non-empty plan fingerprint when configured: %j", (approvedPlanFingerprint) => {
expect(evaluateMissionLineageApproval(snapshot({
planApprovalRequired: true,
task: { approvedPlanFingerprint },
}))).toEqual({ approved: false, reason: "plan-not-approved" });
});
it("accepts a non-empty plan fingerprint when configured", () => {
expect(evaluateMissionLineageApproval(snapshot({
planApprovalRequired: true,
task: { approvedPlanFingerprint: "sha256:approved" },
}))).toEqual({ approved: true, reason: "approved" });
});
it("does not mutate the resolved lineage snapshot", () => {
const input = snapshot({ planApprovalRequired: true, task: { approvedPlanFingerprint: "approved" } });
const before = structuredClone(input);
evaluateMissionLineageApproval(input);
expect(input).toEqual(before);
});
it("exports required statuses matching the predicate contract", () => {
expect(MISSION_LINEAGE_APPROVAL_REQUIRED).toEqual({
missionStatus: "active",
milestoneStatus: "active",
sliceStatus: "active",
featureStatuses: ["triaged", "in-progress"],
});
});
});

View File

@@ -18,6 +18,16 @@ export {
extractSymbolLockIdentity,
symbolLocksConflict,
} from "./task-store/symbol-locks.js";
export {
MISSION_LINEAGE_APPROVAL_REQUIRED,
evaluateMissionLineageApproval,
isMissionLineageApproved,
} from "./symbol-lock-lineage-approval.js";
export type {
MissionLineageApprovalReason,
MissionLineageApprovalResult,
MissionLineageSnapshot,
} from "./symbol-lock-lineage-approval.js";
export { AGENT_VALID_TRANSITIONS, DUPLICATE_OF_METADATA_KEY, assertNotWorkspaceTaskMerge, isWorkspaceTask, WorkspaceTaskMergeError } from "./types.js";
export {
resolveEntryPointBranchAssignment,

View File

@@ -0,0 +1,87 @@
import type { Milestone, Mission, MissionFeature, Slice } from "./mission-types.js";
import type { Task } from "./types.js";
/**
* FNXC:SymbolLock 2026-07-19-14:58:
* Autonomous symbol-lock admission is allowed only for work already greenlit
* through the live Mission → Milestone → Slice → Feature path. An active
* Mission, Milestone, and Slice establish that implementation is currently
* authorized; triaged or in-progress Features are the only implementable
* states. Planning, blocked, completed, archived, defined, and done states
* must not be treated as approval.
*
* When plan approval is required, a non-empty approvedPlanFingerprint proves
* an operator or plan gate approved the task's current plan. This predicate
* checks presence only: plan-approval.ts owns fingerprint computation and
* validation, while this IO-free contract stays reusable by schedulers.
*/
export const MISSION_LINEAGE_APPROVAL_REQUIRED = {
missionStatus: "active",
milestoneStatus: "active",
sliceStatus: "active",
featureStatuses: ["triaged", "in-progress"] as const,
};
export type MissionLineageApprovalReason =
| "approved"
| "missing-mission"
| "missing-milestone"
| "missing-slice"
| "missing-feature"
| "mission-not-active"
| "milestone-not-active"
| "slice-not-active"
| "feature-not-implementable"
| "plan-not-approved";
export type MissionLineageApprovalResult =
| { approved: true; reason: "approved" }
| { approved: false; reason: Exclude<MissionLineageApprovalReason, "approved"> };
/** A resolved lineage only; scheduler-owned resolution deliberately stays outside this pure seam. */
export interface MissionLineageSnapshot {
mission?: Mission | null;
milestone?: Milestone | null;
slice?: Slice | null;
feature?: MissionFeature | null;
task: Pick<Task, "approvedPlanFingerprint">;
planApprovalRequired: boolean;
}
/** Evaluate the canonical symbol-lock admission contract in deterministic failure order. */
export function evaluateMissionLineageApproval(
snapshot: MissionLineageSnapshot,
): MissionLineageApprovalResult {
const { mission, milestone, slice, feature, task, planApprovalRequired } = snapshot;
if (!mission) return { approved: false, reason: "missing-mission" };
if (!milestone) return { approved: false, reason: "missing-milestone" };
if (!slice) return { approved: false, reason: "missing-slice" };
if (!feature) return { approved: false, reason: "missing-feature" };
if (mission.status !== MISSION_LINEAGE_APPROVAL_REQUIRED.missionStatus) {
return { approved: false, reason: "mission-not-active" };
}
if (milestone.status !== MISSION_LINEAGE_APPROVAL_REQUIRED.milestoneStatus) {
return { approved: false, reason: "milestone-not-active" };
}
if (slice.status !== MISSION_LINEAGE_APPROVAL_REQUIRED.sliceStatus) {
return { approved: false, reason: "slice-not-active" };
}
if (!MISSION_LINEAGE_APPROVAL_REQUIRED.featureStatuses.some((status) => status === feature.status)) {
return { approved: false, reason: "feature-not-implementable" };
}
if (
planApprovalRequired
&& (typeof task.approvedPlanFingerprint !== "string" || task.approvedPlanFingerprint.trim().length === 0)
) {
return { approved: false, reason: "plan-not-approved" };
}
return { approved: true, reason: "approved" };
}
/** Convenience projection for callers that do not need the rejection reason. */
export function isMissionLineageApproved(snapshot: MissionLineageSnapshot): boolean {
return evaluateMissionLineageApproval(snapshot).approved;
}