feat: align merge request review workflow with selector refs

This commit is contained in:
2026-08-26 15:03:26 +09:00
parent ccf7de1a55
commit 1d140be715
17 changed files with 376 additions and 109 deletions
@@ -31,6 +31,12 @@ const detailLoader = await Deno.readTextFile(
import.meta.url,
),
);
const detailPage = await Deno.readTextFile(
new URL(
"../../../routes/w/[workspaceId]/merge-requests/[mergeRequestId]/+page.svelte",
import.meta.url,
),
);
const sidebar = await Deno.readTextFile(
new URL("../sidebar/WorkspaceSidebar.svelte", import.meta.url),
);
@@ -64,3 +70,29 @@ Deno.test("Workspace exposes Merge Request collection and detail pages", () => {
"MR resources are absent from navigation",
);
});
Deno.test("Merge Request UI separates source review freshness from target integration", () => {
for (const source of [detailPage, ticketPage]) {
assert(
source.includes("Fresh source review required"),
"missing source-review freshness diagnostic",
);
assert(
source.includes("Target integration"),
"missing target-integration status",
);
assert(
source.includes("Target-only movement") &&
source.includes("does not invalidate approval for an unchanged source"),
"source review and target integration semantics are conflated",
);
}
assert(
detailPage.includes("selector_from moved from"),
"source ref mismatch is not explained",
);
assert(
detailPage.includes("CompleteMergeRequest"),
"target integration authority is not named",
);
});
@@ -1,5 +1,8 @@
<script lang="ts">
import { mergeRequestPagePath } from "$lib/workspace/api/merge-requests";
import {
mergeRequestPagePath,
type MergeRequestDetail,
} from "$lib/workspace/api/merge-requests";
import type { PageData } from "./$types";
let { data }: { data: PageData } = $props();
@@ -13,6 +16,52 @@
const value = event[key];
return typeof value === "string" && value.length > 0 ? value : null;
}
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" || event.subject_ref !== 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}.`;
}
if (
mergeRequest.thread.some(
(event) => event.kind === "review_requested" && event.subject_ref === source,
)
) {
return `Current source review pending for exact ref ${source}.`;
}
return `Fresh source review required: no effective verdict exists for ${source}.`;
}
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.`;
}
</script>
<svelte:head><title>Merge Request · Yoi</title></svelte:head>
@@ -53,6 +102,14 @@
</dl>
</section>
<section class="ticket-detail-section">
<div class="ticket-section-heading"><h2>Review and integration status</h2></div>
<dl class="ticket-facts">
<div><dt>Source review freshness</dt><dd>{sourceReviewFreshness(mergeRequest)}</dd></div>
<div><dt>Target integration</dt><dd>{targetIntegrationStatus(mergeRequest)}</dd></div>
</dl>
</section>
<section class="ticket-detail-section">
<div class="ticket-section-heading">
<h2>Thread</h2><span>{mergeRequest.thread.length}</span>
@@ -556,14 +556,22 @@
<section class="ticket-control-card">
<header><h2>Merge Request</h2></header>
{#if mergeRequest}
<p><strong>{mergeRequest.state}</strong> · review {mergeRequest.review_status}</p>
<p><strong>{mergeRequest.state}</strong></p>
<p>
From <code>{mergeRequest.selector_from ?? "requires repair"}</code>
to <code>{mergeRequest.selector_to}</code>
</p>
{#if mergeRequest.current_subject_ref}
<p>Current source <code>{mergeRequest.current_subject_ref}</code></p>
{#if mergeRequest.current_subject_ref && mergeRequest.review_subject_ref === mergeRequest.current_subject_ref}
<p><strong>Source review:</strong> {mergeRequest.review_status} for exact ref <code>{mergeRequest.current_subject_ref}</code></p>
{:else if mergeRequest.current_subject_ref && mergeRequest.review_subject_ref}
<p><strong>Fresh source review required:</strong> selector_from moved from <code>{mergeRequest.review_subject_ref}</code> to <code>{mergeRequest.current_subject_ref}</code>.</p>
{:else if mergeRequest.current_subject_ref}
<p><strong>Fresh source review required:</strong> no effective verdict exists for <code>{mergeRequest.current_subject_ref}</code>.</p>
{:else}
<p><strong>Source review unavailable:</strong> selector_from is unresolved.</p>
{/if}
<p><strong>Target integration:</strong> {mergeRequest.state === "merged" ? "recorded" : `awaiting Orchestrator integration into ${mergeRequest.selector_to}`}.</p>
<p class="workspace-empty-copy">Target-only movement refreshes integration evidence; it does not invalidate approval for an unchanged source.</p>
<a
class="workspace-secondary-button"
href={mergeRequestPagePath(data.workspaceId, mergeRequest.merge_request_id)}