Files
fusion/packages/core/src/pr-entity.ts
gsxdsm 841ff9d838 Address PR review feedback (#1461)
- pr-nodes: forward post-increment entity to deps.respond (responseRounds off-by-one); branch-group entity fallback now keys on task.branchContext.groupId, not task.id
- core: hoist shared autoMergeGateReason into @fusion/core pr-entity.ts; CLI + dashboard import the single definition (R13)
- cli: fn pr create also writes the unified PR entity (ensure → open), keeping legacy prInfo; resolveBranchHeadOid uses argv-safe execFileAsync (injection fix)
- changeset: fn-pr-commands bumped minor → major (breaking CLI removal per AGENTS.md)
- tests: bin.test.ts daemon fixture off reserved port 4040; lazy-loaded-views title 19 → 20 views

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-06 00:09:53 -07:00

84 lines
3.9 KiB
TypeScript

// Core-owned predicates for the unified PR entity (PR-lifecycle-as-workflow-nodes, U1).
//
// These live in @fusion/core so the dashboard route, the workflow node handlers,
// and the reconcile all consult one definition and cannot drift — the same
// discipline that put isBranchGroupMemberLanded in branch-group-completion.ts.
import type { PrEntity } from "./types.js";
/** Non-terminal lifecycle states — the entity is "live". */
export function isPrEntityActive(entity: Pick<PrEntity, "state">): boolean {
return entity.state !== "merged" && entity.state !== "closed" && entity.state !== "failed";
}
/**
* Whether a piece of work is "PR-backed" for the purpose of keeping it out of
* the legacy merge pipeline.
*
* Merge-target scoping is load-bearing: a shared-group MEMBER landing onto its
* group branch (mergeTargetSource === "branch-group-integration") is NOT
* PR-backed even when its group has an open PR entity — only the group's
* promotion/default-branch merge is. Treating member-integration as PR-backed
* would deadlock the group (members could never land, so it could never complete
* and the PR could never advance). Mirrors how isBranchGroupMemberLanded keys on
* the merge target rather than mere group membership.
*
* An unverified entity (imported legacy state GitHub has not corroborated) still
* counts as PR-backed (R19 hard gate): a possibly-fictional PR must not let the
* task fall back into the legacy merger and risk a double-merge. The reconcile
* clears the fiction and releases the task on its first pass.
*/
export function isPrBacked(
entity: Pick<PrEntity, "state"> | null | undefined,
opts?: { mergeTargetSource?: string },
): boolean {
if (!entity || !isPrEntityActive(entity)) return false;
// Member → group-branch integration is never PR-backed.
if (opts?.mergeTargetSource === "branch-group-integration") return false;
return true;
}
/**
* Whether the entity may participate in auto-merge evaluation or response-run
* dispatch. Unverified entities are frozen until the reconcile corroborates them
* (R19) — they are neither auto-merged nor responded to.
*/
export function isPrEntityActionable(entity: Pick<PrEntity, "state" | "unverified">): boolean {
return isPrEntityActive(entity) && !entity.unverified;
}
/**
* Auto-merge green condition (R10): opted in, approved, all checks concluded
* successful (pending is NOT green), mergeable known-clean (UNKNOWN blocks), and
* verified. Re-evaluated by the auto-merge gate after every push.
*/
export function isPrEntityAutoMergeReady(
entity: Pick<PrEntity, "state" | "unverified" | "autoMerge" | "reviewDecision" | "checksRollup" | "mergeable">,
): boolean {
if (!isPrEntityActionable(entity)) return false;
if (!entity.autoMerge) return false;
if (entity.reviewDecision !== "APPROVED") return false;
if (entity.checksRollup !== "success") return false;
// mergeable must be the known-clean state; "unknown"/conflict/undefined all block.
if (entity.mergeable !== "clean") return false;
return true;
}
/**
* The live auto-merge gate reason shown next to the toggle (R11). Mirrors the
* auto-merge-ready predicate ordering so every surface (the dashboard route and
* the `fn pr` CLI) reports the same status and never disagrees with what the gate
* will actually do. Shared in @fusion/core (R13) so the two surfaces cannot drift.
*/
export function autoMergeGateReason(
entity: Pick<PrEntity, "state" | "unverified" | "autoMerge" | "reviewDecision" | "checksRollup" | "mergeable">,
): string {
if (!entity.autoMerge) return "Auto-merge off";
if (entity.mergeable === "conflicting") return "Blocked: conflict";
if (entity.reviewDecision !== "APPROVED") return "Waiting for approval";
if (entity.checksRollup !== "success") return "Waiting for checks";
if (entity.mergeable !== "clean") return "Waiting for checks";
if (isPrEntityAutoMergeReady(entity)) return "Ready to merge";
return "Waiting for checks";
}