diff --git a/web/workspace/src/lib/workspace/merge-request-status.ts b/web/workspace/src/lib/workspace/merge-request-status.ts
new file mode 100644
index 00000000..536476e7
--- /dev/null
+++ b/web/workspace/src/lib/workspace/merge-request-status.ts
@@ -0,0 +1,76 @@
+import type {
+ MergeRequestDetail,
+ MergeRequestThreadEvent,
+} from "./api/merge-requests.ts";
+
+function isCurrentSourceEvent(
+ event: MergeRequestThreadEvent,
+ source: string,
+): boolean {
+ return event.subject_ref === source;
+}
+
+function requestHasTerminalOutcome(
+ thread: MergeRequestThreadEvent[],
+ requestEventId: unknown,
+): boolean {
+ return thread.some((event) =>
+ (event.kind === "review" || event.kind === "review_cancelled") &&
+ event.request_event_id === requestEventId
+ );
+}
+
+export function sourceReviewFreshness(
+ mergeRequest: MergeRequestDetail,
+): string {
+ const source = mergeRequest.source.ref;
+ if (!source) return "Source review unavailable: selector_from is unresolved.";
+
+ const effectiveReview = [...mergeRequest.thread].reverse().find((event) => {
+ if (event.kind !== "review" || !isCurrentSourceEvent(event, source)) {
+ return false;
+ }
+ return !mergeRequest.thread.some(
+ (candidate) =>
+ candidate.kind === "review_revoked" &&
+ candidate.review_event_id === event.event_id,
+ );
+ });
+ if (effectiveReview) {
+ return effectiveReview.decision === "approve"
+ ? `Current source approved at exact ref ${source}.`
+ : `Current source requests changes at exact ref ${source}.`;
+ }
+
+ const latestEvidence = [...mergeRequest.thread].reverse().find(
+ (event) =>
+ (event.kind === "review" || event.kind === "review_requested") &&
+ typeof event.subject_ref === "string",
+ );
+ if (latestEvidence?.subject_ref && latestEvidence.subject_ref !== source) {
+ return `Fresh source review required: selector_from moved from ${latestEvidence.subject_ref} to ${source}.`;
+ }
+
+ const pendingRequest = [...mergeRequest.thread].reverse().find((event) =>
+ event.kind === "review_requested" &&
+ isCurrentSourceEvent(event, source) &&
+ !requestHasTerminalOutcome(mergeRequest.thread, event.event_id)
+ );
+ if (pendingRequest) {
+ return `Current source review pending for exact ref ${source}.`;
+ }
+
+ return `Fresh source review required: no effective verdict exists for ${source}.`;
+}
+
+export function targetIntegrationStatus(
+ mergeRequest: MergeRequestDetail,
+): string {
+ if (mergeRequest.state === "merged") {
+ return "Target integration recorded by CompleteMergeRequest.";
+ }
+ if (!mergeRequest.target.ref) {
+ return "Target integration unavailable: selector_to is unresolved.";
+ }
+ return `Target integration awaits Orchestrator action at ${mergeRequest.target.ref}. Target-only movement refreshes integration evidence; it does not invalidate approval for an unchanged source.`;
+}
diff --git a/web/workspace/src/lib/workspace/tickets/merge-request-resources.test.ts b/web/workspace/src/lib/workspace/tickets/merge-request-resources.test.ts
index af3db5e6..4fb5681a 100644
--- a/web/workspace/src/lib/workspace/tickets/merge-request-resources.test.ts
+++ b/web/workspace/src/lib/workspace/tickets/merge-request-resources.test.ts
@@ -37,6 +37,9 @@ const detailPage = await Deno.readTextFile(
import.meta.url,
),
);
+const statusProjection = await Deno.readTextFile(
+ new URL("../merge-request-status.ts", import.meta.url),
+);
const sidebar = await Deno.readTextFile(
new URL("../sidebar/WorkspaceSidebar.svelte", import.meta.url),
);
@@ -72,7 +75,12 @@ Deno.test("Workspace exposes Merge Request collection and detail pages", () => {
});
Deno.test("Merge Request UI separates source review freshness from target integration", () => {
- for (const source of [detailPage, ticketPage]) {
+ assert(
+ detailPage.includes("sourceReviewFreshness") &&
+ detailPage.includes("targetIntegrationStatus"),
+ "MR detail page does not render authority status projections",
+ );
+ for (const source of [`${detailPage}\n${statusProjection}`, ticketPage]) {
assert(
source.includes("Fresh source review required"),
"missing source-review freshness diagnostic",
@@ -88,11 +96,11 @@ Deno.test("Merge Request UI separates source review freshness from target integr
);
}
assert(
- detailPage.includes("selector_from moved from"),
+ statusProjection.includes("selector_from moved from"),
"source ref mismatch is not explained",
);
assert(
- detailPage.includes("CompleteMergeRequest"),
+ statusProjection.includes("CompleteMergeRequest"),
"target integration authority is not named",
);
});
diff --git a/web/workspace/src/routes/w/[workspaceId]/merge-requests/[mergeRequestId]/+page.svelte b/web/workspace/src/routes/w/[workspaceId]/merge-requests/[mergeRequestId]/+page.svelte
index 193ffcce..f8b86b83 100644
--- a/web/workspace/src/routes/w/[workspaceId]/merge-requests/[mergeRequestId]/+page.svelte
+++ b/web/workspace/src/routes/w/[workspaceId]/merge-requests/[mergeRequestId]/+page.svelte
@@ -1,8 +1,9 @@
Merge Request ยท Yoi
diff --git a/web/workspace/test/merge-request-status.test.ts b/web/workspace/test/merge-request-status.test.ts
new file mode 100644
index 00000000..6436396f
--- /dev/null
+++ b/web/workspace/test/merge-request-status.test.ts
@@ -0,0 +1,142 @@
+///
+
+import type {
+ MergeRequestDetail,
+ MergeRequestThreadEvent,
+} from "../src/lib/workspace/api/merge-requests.ts";
+import { sourceReviewFreshness } from "../src/lib/workspace/merge-request-status.ts";
+
+function assertEquals(actual: unknown, expected: unknown): void {
+ if (actual !== expected) {
+ throw new Error(
+ `expected ${JSON.stringify(expected)}, got ${JSON.stringify(actual)}`,
+ );
+ }
+}
+
+function event(
+ kind: string,
+ fields: Record,
+): MergeRequestThreadEvent {
+ return { kind, sequence: 1, at: "2026-09-01T00:00:00Z", ...fields };
+}
+
+function detail(thread: MergeRequestThreadEvent[]): MergeRequestDetail {
+ return {
+ merge_request_id: "MR-1",
+ workspace_id: "W",
+ repository_id: "main",
+ ticket_ids: ["T-1"],
+ selector_from: "work/ticket",
+ selector_to: "develop",
+ state: "open",
+ opened_by: {
+ runtime_id: "runtime",
+ worker_id: "worker",
+ assignment_id: "assignment",
+ },
+ created_at: "2026-09-01T00:00:00Z",
+ updated_at: "2026-09-01T00:00:00Z",
+ source: {
+ status: "known",
+ ref: "source-2",
+ observed_at: "2026-09-01T00:00:00Z",
+ },
+ target: {
+ status: "known",
+ ref: "target-2",
+ observed_at: "2026-09-01T00:00:00Z",
+ },
+ linked_tickets: [{ ticket_id: "T-1", key: "T-1" }],
+ thread,
+ };
+}
+
+Deno.test("revoked review requires a fresh review instead of appearing pending", () => {
+ const mergeRequest = detail([
+ event("review_requested", {
+ event_id: "request-1",
+ subject_ref: "source-2",
+ }),
+ event("review", {
+ event_id: "review-1",
+ request_event_id: "request-1",
+ subject_ref: "source-2",
+ decision: "approve",
+ }),
+ event("review_revoked", {
+ event_id: "revoke-1",
+ review_event_id: "review-1",
+ }),
+ ]);
+
+ assertEquals(
+ sourceReviewFreshness(mergeRequest),
+ "Fresh source review required: no effective verdict exists for source-2.",
+ );
+});
+
+Deno.test("unresolved review request for the current source is pending", () => {
+ const mergeRequest = detail([
+ event("review_requested", {
+ event_id: "request-2",
+ subject_ref: "source-2",
+ }),
+ ]);
+
+ assertEquals(
+ sourceReviewFreshness(mergeRequest),
+ "Current source review pending for exact ref source-2.",
+ );
+});
+
+Deno.test("completed or cancelled request is not projected as pending", () => {
+ const approved = detail([
+ event("review_requested", {
+ event_id: "request-3",
+ subject_ref: "source-2",
+ }),
+ event("review", {
+ event_id: "review-3",
+ request_event_id: "request-3",
+ subject_ref: "source-2",
+ decision: "approve",
+ }),
+ ]);
+ assertEquals(
+ sourceReviewFreshness(approved),
+ "Current source approved at exact ref source-2.",
+ );
+
+ const cancelled = detail([
+ event("review_requested", {
+ event_id: "request-4",
+ subject_ref: "source-2",
+ }),
+ event("review_cancelled", {
+ event_id: "cancel-4",
+ request_event_id: "request-4",
+ subject_ref: "source-2",
+ }),
+ ]);
+ assertEquals(
+ sourceReviewFreshness(cancelled),
+ "Fresh source review required: no effective verdict exists for source-2.",
+ );
+});
+
+Deno.test("source movement explains the exact stale and current refs", () => {
+ const mergeRequest = detail([
+ event("review", {
+ event_id: "review-old",
+ request_event_id: "request-old",
+ subject_ref: "source-1",
+ decision: "approve",
+ }),
+ ]);
+
+ assertEquals(
+ sourceReviewFreshness(mergeRequest),
+ "Fresh source review required: selector_from moved from source-1 to source-2.",
+ );
+});