fix(ticket): project immutable results and typed source observations

T-716: merged evidence uses recorded result approval; open source reads use Runtime provider authority and preserve unavailability. Current requirements use the shared exact snapshot evaluator. Remove primary-MR/report SQL heuristics; continue bounded query scans through nonmatching candidates. Align Worker/Web projections and explain lifecycle versus integration/attestation. Synchronize checker test on delivered inputs, not pre-delivery attempt state.

Validation: root cargo check; cargo test -p merge-request (30), -p server-api (70), -p worker (949), -p yoi-workspace-server (620 lib + 11 binary); server-api typescript feature check; 32 focused Deno tests; deno task check (0 errors/warnings); cargo fmt --all -- --check; git diff --check HEAD; canonical OpenAPI/generated Ticket artifact checks.
This commit is contained in:
2026-10-08 15:44:27 +09:00
parent 4d0f2afc1e
commit 5424280348
24 changed files with 2096 additions and 517 deletions
@@ -1,6 +1,6 @@
// @generated by yoi-companion-openapi-typescript@1. Do not edit.
// Input: openapi/server-api.json
// Input contract: 0.1.0 (sha256:a6a7147c9a2f465a5da044e9ba21947cb3d86eacbfe3905696483917e794d5cf)
// Input contract: 0.1.0 (sha256:4f468e00efa86e6e33e8fa4f14deca93ee15b955071920facd56c59c3d2fefb8)
// Options: companion-operation-schema-closure,openapi-3.1,strict-integer-ranges,sorted-output
// Output: web/workspace/src/lib/generated/companion-api.ts
// Regenerate/check: cargo run -q -p server-api --example generate_companion_api_types -- --check
@@ -1,6 +1,6 @@
// @generated by yoi-repository-openapi-typescript@1. Do not edit.
// Input: openapi/server-api.json
// Input contract: 0.1.0 (sha256:a6a7147c9a2f465a5da044e9ba21947cb3d86eacbfe3905696483917e794d5cf)
// Input contract: 0.1.0 (sha256:4f468e00efa86e6e33e8fa4f14deca93ee15b955071920facd56c59c3d2fefb8)
// Options: repository-operation-schema-closure,openapi-3.1,format:uint64=safe-number,strict-safe-integers,sorted-output
// Output: web/workspace/src/lib/generated/repository-api.ts
// Regenerate/check: cargo run -q -p server-api --example generate_repository_openapi_types -- --check
@@ -1,6 +1,6 @@
// @generated by yoi-runtime-openapi-typescript@1. Do not edit.
// Input: openapi/server-api.json
// Input contract: 0.1.0 (sha256:a6a7147c9a2f465a5da044e9ba21947cb3d86eacbfe3905696483917e794d5cf)
// Input contract: 0.1.0 (sha256:4f468e00efa86e6e33e8fa4f14deca93ee15b955071920facd56c59c3d2fefb8)
// Options: runtime-operation-schema-closure,openapi-3.1,strict-integer-ranges,sorted-output
// Output: web/workspace/src/lib/generated/runtime-api.ts
// Regenerate/check: cargo run -q -p server-api --example generate_runtime_api_types -- --check
@@ -1,6 +1,6 @@
// @generated by yoi-ticket-browser-openapi-typescript@1. Do not edit.
// Input: openapi/server-api.json
// Input contract: 0.1.0 (sha256:a6a7147c9a2f465a5da044e9ba21947cb3d86eacbfe3905696483917e794d5cf)
// Input contract: 0.1.0 (sha256:4f468e00efa86e6e33e8fa4f14deca93ee15b955071920facd56c59c3d2fefb8)
// Options: ticket-objective-merge-request-browser-operation-schema-closure,openapi-3.1,strict-integer-ranges,sorted-output
// Output: web/workspace/src/lib/generated/ticket-api.ts
// Regenerate/check: cargo run -q -p server-api --example generate_ticket_api_types -- --check
@@ -117,7 +117,7 @@ export type TicketListResponse = { invalid_records: Array<InvalidProjectRecord>;
export type TicketMarkReadyRequest = { intake_summary?: TicketIntakeSummary | null; operation_key: string; reason?: string | null; };
export type TicketMergeRequestSummary = { current_subject_ref?: string | null; merge_request_id: string; repository_key: string; review_excerpt?: string | null; review_requested_at?: string | null; review_status: string; review_subject_ref?: string | null; review_submitted_at?: string | null; selector_from?: string | null; selector_to: string; state: string; updated_at: string; };
export type TicketMergeRequestSummary = { current_subject_ref?: string | null; integration_evidence_error?: string | null; merge_request_id: string; repository_key: string; review_excerpt?: string | null; review_requested_at?: string | null; review_status: string; review_subject_ref?: string | null; review_submitted_at?: string | null; selector_from?: string | null; selector_to: string; source_ref_observation: TicketSourceRefObservation; state: string; updated_at: string; };
export type TicketQueueOutcome = { queued_tickets: Array<string>; requested_ticket: string; };
@@ -131,6 +131,8 @@ export type TicketRoleAssignmentRecord = { assigned_at: string; assigned_by: str
export type TicketRoleAssignmentSummary = { assigned_at: string; assigned_by: string; assignment_id: string; principal: TicketAssignmentPrincipal; role: string; };
export type TicketSourceRefObservation = { status: "observed"; } | { code: string; status: "unavailable"; } | { status: "not_required"; };
export type TicketStatus = "Open" | "Closed";
export type TicketTarget = { access: TicketTargetAccess; ref_selector?: string | null; repository_key: string; };
@@ -1,6 +1,6 @@
// @generated by yoi-workdir-openapi-typescript@1. Do not edit.
// Input: openapi/server-api.json
// Input contract: 0.1.0 (sha256:fd06f382c6d33a5c3cc98ef9e264545923c53fa95a9be555a85680f98c6065da)
// Input contract: 0.1.0 (sha256:4f468e00efa86e6e33e8fa4f14deca93ee15b955071920facd56c59c3d2fefb8)
// Options: workdir-operation-schema-closure,openapi-3.1,strict-integer-ranges,sorted-output
// Output: web/workspace/src/lib/generated/workdir-api.ts
// Regenerate/check: cargo run -q -p server-api --example generate_workdir_api_types -- --check
@@ -1,6 +1,6 @@
// @generated by yoi-worker-openapi-typescript@1. Do not edit.
// Input: openapi/server-api.json
// Input contract: 0.1.0 (sha256:a6a7147c9a2f465a5da044e9ba21947cb3d86eacbfe3905696483917e794d5cf)
// Input contract: 0.1.0 (sha256:4f468e00efa86e6e33e8fa4f14deca93ee15b955071920facd56c59c3d2fefb8)
// Options: worker-operation-schema-closure,openapi-3.1,strict-integer-ranges,sorted-output
// Output: web/workspace/src/lib/generated/worker-launch-api.ts
// Regenerate/check: cargo run -q -p server-api --example generate_worker_launch_api_types -- --check
@@ -4,6 +4,7 @@ declare const Deno: {
import {
parseMergeRequestDetailResponse,
parseMergeRequestListResponse,
parseObjectiveListResponse,
parseTicketDetail,
parseTicketListResponse,
@@ -80,8 +81,8 @@ Deno.test("Ticket Browser parser rejects unknown fields and out-of-range limits"
);
});
Deno.test("Ticket Browser parser accepts target collections and rejects singular target authority", () => {
const fixture = {
function ticketDetailFixture() {
return {
action_eligibility: {
blockers: [],
can_assign_orchestrator: false,
@@ -142,7 +143,10 @@ Deno.test("Ticket Browser parser accepts target collections and rejects singular
],
title: "Ticket",
};
}
Deno.test("Ticket Browser parser accepts target collections and rejects singular target authority", () => {
const fixture = ticketDetailFixture();
const parsed = parseTicketDetail(fixture);
assertEquals(parsed.targets.length, 2);
assertEquals(parsed.targets[0]?.repository_key, "docs");
@@ -168,6 +172,157 @@ Deno.test("Ticket Browser parser accepts target collections and rejects singular
);
});
const mergeRequestSummary = {
merge_request_id: "mr-1",
repository_key: "main",
state: "open",
selector_from: "work/T-1",
selector_to: "develop",
updated_at: "2026-10-08T00:00:00Z",
current_subject_ref: "source-1",
source_ref_observation: { status: "observed" },
integration_evidence_error: null,
review_status: "approved",
review_subject_ref: "source-1",
review_requested_at: null,
review_submitted_at: null,
review_excerpt: null,
};
function mergeRequestListFixture(summary: unknown) {
return {
items: [{ summary, ticket_ids: ["ticket-id"], thread_event_count: 2 }],
next_cursor: null,
};
}
Deno.test("Merge Request summaries preserve live observation and immutable merged evidence separately", () => {
for (
const summary of [
mergeRequestSummary,
{
...mergeRequestSummary,
current_subject_ref: null,
source_ref_observation: {
status: "unavailable",
code: "repository_unavailable",
},
review_status: "pending",
},
{
...mergeRequestSummary,
state: "merged",
source_ref_observation: { status: "not_required" },
},
{
...mergeRequestSummary,
state: "merged",
source_ref_observation: { status: "not_required" },
integration_evidence_error: "approval_event_missing",
review_status: "none",
},
]
) {
const listed = parseMergeRequestListResponse(
mergeRequestListFixture(summary),
);
const detailed = parseTicketDetail({
...ticketDetailFixture(),
merge_requests: [summary],
merge_request: summary,
});
for (
const parsed of [
listed.items[0]?.summary,
detailed.merge_requests[0],
detailed.merge_request,
]
) {
assertEquals(
parsed?.source_ref_observation,
summary.source_ref_observation,
);
assertEquals(parsed?.current_subject_ref, summary.current_subject_ref);
assertEquals(
parsed?.integration_evidence_error,
summary.integration_evidence_error,
);
assertEquals(parsed?.review_subject_ref, summary.review_subject_ref);
}
}
});
Deno.test("Merge Request summaries reject missing and malformed tagged source observations", () => {
for (
const [observation, message] of [
[undefined, "source_ref_observation must be an object"],
[null, "source_ref_observation must be an object"],
["observed", "source_ref_observation must be an object"],
[{ status: "unknown" }, "status is invalid"],
[{ status: "unavailable" }, "code must be a string"],
[{ status: "unavailable", code: 42 }, "code must be a string"],
[
{ status: "unavailable", code: "x".repeat(262_145) },
"code exceeds its length limit",
],
[{ status: "observed", code: "stale" }, "unknown field code"],
[{ status: "not_required", code: "stale" }, "unknown field code"],
[
{ status: "unavailable", code: "missing", extra: true },
"unknown field extra",
],
] as const
) {
assertThrows(
() =>
parseMergeRequestListResponse(mergeRequestListFixture({
...mergeRequestSummary,
source_ref_observation: observation,
})),
message,
);
}
});
Deno.test("Merge Request summaries require a nullable bounded integration evidence error", () => {
for (
const [error, message] of [
[undefined, "integration_evidence_error must be a string"],
[false, "integration_evidence_error must be a string"],
[
"x".repeat(262_145),
"integration_evidence_error exceeds its length limit",
],
] as const
) {
assertThrows(
() =>
parseMergeRequestListResponse(mergeRequestListFixture({
...mergeRequestSummary,
integration_evidence_error: error,
})),
message,
);
}
});
Deno.test("done Ticket state is preserved when current requirement evidence is incomplete", () => {
const fixture = ticketDetailFixture();
const parsed = parseTicketDetail({
...fixture,
state: "done",
evidence: { ...fixture.evidence, missing: ["review_after_rescope"] },
merge_requests: [{
...mergeRequestSummary,
state: "merged",
source_ref_observation: { status: "not_required" },
}],
});
assertEquals(parsed.state, "done");
assertEquals(parsed.evidence.complete_for_integration, false);
assertEquals(parsed.merge_requests[0]?.review_status, "approved");
});
Deno.test("Ticket Browser parser validates Ticket create responses", () => {
const parsed = parseTicketRecordRef({
id: "ticket-id",
@@ -446,6 +446,23 @@ function parseActionEligibility(
};
}
function parseSourceRefObservation(
value: unknown,
label: string,
): TicketMergeRequestSummary["source_ref_observation"] {
const item = object(value, label);
const status = string(item.status, `${label}.status`);
if (status === "observed" || status === "not_required") {
exact(item, ["status"], label);
return { status };
}
if (status === "unavailable") {
exact(item, ["status", "code"], label);
return { status, code: string(item.code, `${label}.code`) };
}
throw new Error(`${label}.status is invalid`);
}
function parseTicketMergeRequestSummary(
value: unknown,
label: string,
@@ -453,6 +470,8 @@ function parseTicketMergeRequestSummary(
const item = object(value, label);
exact(item, [
"current_subject_ref",
"integration_evidence_error",
"source_ref_observation",
"merge_request_id",
"repository_key",
"review_excerpt",
@@ -470,6 +489,14 @@ function parseTicketMergeRequestSummary(
item.current_subject_ref,
`${label}.current_subject_ref`,
),
integration_evidence_error: nullableString(
item.integration_evidence_error,
`${label}.integration_evidence_error`,
),
source_ref_observation: parseSourceRefObservation(
item.source_ref_observation,
`${label}.source_ref_observation`,
),
merge_request_id: string(
item.merge_request_id,
`${label}.merge_request_id`,
@@ -181,6 +181,8 @@ export function dashboardFixture(
review_requested_at: updated(10),
review_submitted_at: null,
current_subject_ref: "abc123",
source_ref_observation: { status: "observed" },
integration_evidence_error: null,
review_subject_ref: "abc123",
},
thread_event_count: 1,
@@ -0,0 +1,219 @@
declare const Deno: {
test(name: string, fn: () => Promise<void> | void): void;
readTextFile(path: URL): Promise<string>;
};
import type { TicketMergeRequestSummary } from "#lib/generated/ticket-api.ts";
import { fixtureDetail } from "./home/dashboard.test-fixtures.ts";
import { parseTicketDetail } from "./api/ticket-browser.ts";
import {
sourceReviewFreshness,
summarySourceReviewStatus,
} from "./merge-request-status.ts";
import type { MergeRequestDetail } from "./api/merge-requests.ts";
function assertEquals(actual: unknown, expected: unknown): void {
if (actual !== expected) {
throw new Error(
`Expected ${JSON.stringify(expected)}, received ${
JSON.stringify(actual)
}`,
);
}
}
const summary: TicketMergeRequestSummary = {
merge_request_id: "mr-1",
repository_key: "main",
state: "open",
selector_from: "work/T-1",
selector_to: "develop",
updated_at: "2026-10-08T00:00:00Z",
current_subject_ref: "source-1",
source_ref_observation: { status: "observed" },
integration_evidence_error: null,
review_status: "approved",
review_subject_ref: "source-1",
};
Deno.test("unavailable live observation shows its error rather than claiming review is stale or pending", () => {
assertEquals(
summarySourceReviewStatus({
...summary,
current_subject_ref: null,
source_ref_observation: {
status: "unavailable",
code: "repository_unavailable",
},
review_status: "pending",
}),
"Live source observation unavailable: repository_unavailable.",
);
});
Deno.test("merged summary labels approval as immutable persisted evidence without live observation", () => {
assertEquals(
summarySourceReviewStatus({
...summary,
state: "merged",
source_ref_observation: { status: "not_required" },
}),
"Integration approval recorded for immutable merged source source-1.",
);
});
Deno.test("merged evidence error does not claim a valid integration approval", () => {
assertEquals(
summarySourceReviewStatus({
...summary,
state: "merged",
source_ref_observation: { status: "not_required" },
integration_evidence_error: "approval_event_missing",
}),
"Integration approval unavailable for immutable merged source source-1.",
);
});
Deno.test("observed live source movement requires fresh review but an unchanged ref retains approval", () => {
assertEquals(
summarySourceReviewStatus(summary),
"Live source review: approved for exact ref source-1.",
);
assertEquals(
summarySourceReviewStatus({ ...summary, current_subject_ref: "source-2" }),
"Fresh live source review required: selector_from moved from source-1 to source-2.",
);
});
Deno.test("done Ticket can retain immutable approval while current requirement evidence is incomplete", () => {
const ticket = parseTicketDetail({
...fixtureDetail(),
state: "done",
evidence: {
...fixtureDetail().evidence,
missing: ["review_after_rescope"],
},
merge_requests: [{
...summary,
state: "merged",
source_ref_observation: { status: "not_required" },
}],
});
assertEquals(ticket.state, "done");
assertEquals(ticket.evidence.complete_for_integration, false);
assertEquals(
summarySourceReviewStatus(ticket.merge_requests[0]!),
"Integration approval recorded for immutable merged source source-1.",
);
});
Deno.test("Ticket route separates current requirement evidence from immutable integration approval without cancelling done", async () => {
const source = await Deno.readTextFile(
new URL(
"../../routes/w/[workspaceId]/tickets/[ticketId]/+page.svelte",
import.meta.url,
),
);
for (
const required of [
"Current requirement evidence",
"ticket.evidence.complete_for_integration",
"ticket.evidence.approved_current_subject",
"ticket.evidence.missing",
"summarySourceReviewStatus(mergeRequest)",
"Immutable integration evidence",
"Integration evidence error:",
'{#if ticket.state === "done"}',
"Incomplete current requirement evidence does not cancel recorded completion.",
]
) {
if (!source.includes(required)) {
throw new Error(`Ticket route must include ${required}`);
}
}
});
Deno.test("Merge Request routes distinguish recorded approval from live source observation and expose errors", async () => {
const list = await Deno.readTextFile(
new URL(
"../../routes/w/[workspaceId]/merge-requests/+page.svelte",
import.meta.url,
),
);
assertEquals(list.includes("summarySourceReviewStatus(mergeRequest)"), true);
assertEquals(list.includes("Integration evidence error:"), true);
const detail = await Deno.readTextFile(
new URL(
"../../routes/w/[workspaceId]/merge-requests/[mergeRequestId]/+page.svelte",
import.meta.url,
),
);
assertEquals(detail.includes("Recorded integration approval"), true);
assertEquals(detail.includes("Live source revision"), true);
assertEquals(detail.includes("Source observation error"), true);
});
Deno.test("merged detail uses backend validated immutable evidence even with a partial thread", () => {
const detail: MergeRequestDetail = {
merge_request_id: "mr-1",
workspace_id: "workspace-a",
repository_key: "main",
ticket_ids: [],
linked_tickets: [],
selector_from: "work/T-1",
selector_to: "develop",
state: "merged",
created_at: summary.updated_at,
updated_at: summary.updated_at,
source: {
status: "known",
ref: "source-1",
observed_at: summary.updated_at,
},
target: {
status: "known",
ref: "target-2",
observed_at: summary.updated_at,
},
thread: [{
kind: "merge",
event_id: "merge-1",
sequence: 3,
created_at: summary.updated_at,
operation_id: "operation-1",
approval_event_id: "approval-1",
approved_source_ref: "source-1",
target_ref_before: "target-1",
target_ref_after: "target-2",
strategy: "fast_forward",
resolution: "none",
merged_by: { runtime_id: "runtime-a", worker_id: "worker-a" },
}],
};
assertEquals(
sourceReviewFreshness(detail),
"Recorded integration approval approval-1 for immutable merged source source-1.",
);
assertEquals(
sourceReviewFreshness({ ...detail, thread: [] }),
"Recorded integration approval for immutable merged source source-1.",
);
assertEquals(
sourceReviewFreshness({
...detail,
source: {
...detail.source,
status: "unknown",
diagnostic: { code: "approval_revoked", message: "Revoked" },
},
}),
"Immutable integration evidence unavailable: approval_revoked.",
);
assertEquals(
sourceReviewFreshness({
...detail,
source: { ...detail.source, status: "unknown", ref: null },
}),
"Immutable integration evidence unavailable: no validated persisted source.",
);
});
@@ -1,3 +1,4 @@
import type { TicketMergeRequestSummary } from "#lib/generated/ticket-api.ts";
import type {
MergeRequestDetail,
MergeRequestThreadEvent,
@@ -29,6 +30,24 @@ function requestHasTerminalOutcome(
export function sourceReviewFreshness(
mergeRequest: MergeRequestDetail,
): string {
if (mergeRequest.state === "merged") {
// The Backend validates the full persisted result before paging the thread.
// A merge event in a page alone is not evidence of a valid approval.
if (mergeRequest.source.diagnostic) {
return `Immutable integration evidence unavailable: ${mergeRequest.source.diagnostic.code}.`;
}
const source = mergeRequest.source.ref;
if (mergeRequest.source.status !== "known" || !source) {
return "Immutable integration evidence unavailable: no validated persisted source.";
}
const merge = [...mergeRequest.thread].reverse().find(
(event): event is Extract<MergeRequestThreadEvent, { kind: "merge" }> =>
event.kind === "merge",
);
return merge
? `Recorded integration approval ${merge.approval_event_id} for immutable merged source ${source}.`
: `Recorded integration approval for immutable merged source ${source}.`;
}
const source = mergeRequest.source.ref;
if (!source) return "Source review unavailable: selector_from is unresolved.";
@@ -70,6 +89,41 @@ export function sourceReviewFreshness(
return `Fresh source review required: no effective verdict exists for ${source}.`;
}
export function summarySourceReviewStatus(
mergeRequest: TicketMergeRequestSummary,
): string {
const source = mergeRequest.current_subject_ref;
if (mergeRequest.state === "merged") {
if (!source) return "Immutable merged source unavailable.";
if (
mergeRequest.integration_evidence_error !== null ||
mergeRequest.review_status !== "approved" ||
mergeRequest.review_subject_ref !== source
) {
return `Integration approval unavailable for immutable merged source ${source}.`;
}
return `Integration approval recorded for immutable merged source ${source}.`;
}
const observation = mergeRequest.source_ref_observation;
if (observation.status === "unavailable") {
return `Live source observation unavailable: ${observation.code}.`;
}
if (observation.status === "not_required") {
return "Live source observation not required.";
}
if (!source) {
return "Live source review unavailable: no source ref was observed.";
}
if (mergeRequest.review_subject_ref === source) {
return `Live source review: ${mergeRequest.review_status} for exact ref ${source}.`;
}
if (mergeRequest.review_subject_ref) {
return `Fresh live source review required: selector_from moved from ${mergeRequest.review_subject_ref} to ${source}.`;
}
return `Fresh live source review required: no effective verdict exists for ${source}.`;
}
export function targetIntegrationStatus(
mergeRequest: MergeRequestDetail,
): string {
@@ -80,7 +80,16 @@ Deno.test("Merge Request UI separates source review freshness from target integr
detailPage.includes("targetIntegrationStatus"),
"MR detail page does not render authority status projections",
);
for (const source of [`${detailPage}\n${statusProjection}`, ticketPage]) {
assert(
ticketPage.includes("summarySourceReviewStatus(mergeRequest)"),
"Ticket page does not render the source observation/integration evidence projection",
);
for (
const source of [
`${detailPage}\n${statusProjection}`,
`${ticketPage}\n${statusProjection}`,
]
) {
assert(
source.includes("Fresh source review required"),
"missing source-review freshness diagnostic",
@@ -1,5 +1,6 @@
<script lang="ts">
import { mergeRequestPagePath } from "#lib/workspace/api/merge-requests.ts";
import { summarySourceReviewStatus } from "#lib/workspace/merge-request-status.ts";
import type { PageData } from "./$types";
let { data }: { data: PageData } = $props();
@@ -40,8 +41,12 @@
</strong>
</div>
<p class="ticket-summary">
Repository {mergeRequest.repository_key} · {item.ticket_ids.length} linked Ticket{item.ticket_ids.length === 1 ? "" : "s"} · review {mergeRequest.review_status}
Repository {mergeRequest.repository_key} · {item.ticket_ids.length} linked Ticket{item.ticket_ids.length === 1 ? "" : "s"}
</p>
<p class="ticket-summary">{summarySourceReviewStatus(mergeRequest)}</p>
{#if mergeRequest.integration_evidence_error !== null}
<p class="workspace-callout is-error">Integration evidence error: {mergeRequest.integration_evidence_error}</p>
{/if}
</div>
<div class="ticket-meta">
<span class={`ticket-state state-${mergeRequest.state}`}>{mergeRequest.state}</span>
@@ -59,7 +59,10 @@
<div><dt>Repository</dt><dd>{mergeRequest.repository_key}</dd></div>
<div><dt>State</dt><dd>{mergeRequest.state}</dd></div>
<div><dt>Source selector</dt><dd><code>{mergeRequest.selector_from ?? "requires repair"}</code></dd></div>
<div><dt>Source revision</dt><dd>{mergeRequest.source.status}{mergeRequest.source.ref ? ` · ${mergeRequest.source.ref}` : ""}</dd></div>
<div><dt>Live source revision</dt><dd>{mergeRequest.source.status}{mergeRequest.source.ref ? ` · ${mergeRequest.source.ref}` : ""}</dd></div>
{#if mergeRequest.source.diagnostic}
<div><dt>Source observation error</dt><dd>{mergeRequest.source.diagnostic.code}: {mergeRequest.source.diagnostic.message}</dd></div>
{/if}
<div><dt>Target selector</dt><dd><code>{mergeRequest.selector_to}</code></dd></div>
<div><dt>Target revision</dt><dd>{mergeRequest.target.status}{mergeRequest.target.ref ? ` · ${mergeRequest.target.ref}` : ""}</dd></div>
<div><dt>Updated</dt><dd>{prettyDate(mergeRequest.updated_at)}</dd></div>
@@ -69,7 +72,7 @@
<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>{mergeRequest.state === "merged" ? "Recorded integration approval" : "Live source review freshness"}</dt><dd>{sourceReviewFreshness(mergeRequest)}</dd></div>
<div><dt>Target integration</dt><dd>{targetIntegrationStatus(mergeRequest)}</dd></div>
</dl>
</section>
@@ -13,6 +13,7 @@
parseTicketRoleAssignmentMutationResponse,
} from "#lib/workspace/api/ticket-browser.ts";
import { mergeRequestPagePath } from "#lib/workspace/api/merge-requests.ts";
import { summarySourceReviewStatus } from "#lib/workspace/merge-request-status.ts";
import {
relationLabel,
TICKET_STATES,
@@ -681,6 +682,21 @@
</form>
</details>
<section class="ticket-control-card">
<header><h2>Current requirement evidence</h2></header>
<p><strong>Current requirements:</strong> {ticket.evidence.complete_for_integration ? "satisfied for integration" : "not satisfied for integration"}.</p>
<p><strong>Current requirement approval:</strong> {ticket.evidence.approved_current_subject ? "approved" : "not established"}.</p>
{#if ticket.evidence.missing.length > 0}
<ul>
{#each ticket.evidence.missing as missing}<li>{missing}</li>{/each}
</ul>
{/if}
<p class="workspace-empty-copy">This evidence evaluates current Ticket requirements, separately from persisted integration approval.</p>
{#if ticket.state === "done"}
<p class="workspace-empty-copy">This Ticket remains done. Incomplete current requirement evidence does not cancel recorded completion.</p>
{/if}
</section>
<section class="ticket-control-card">
<header><h2>Merge Requests</h2></header>
{#if mergeRequests.length > 0}
@@ -691,17 +707,16 @@
From <code>{mergeRequest.selector_from ?? "requires repair"}</code>
to <code>{mergeRequest.selector_to}</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>
<p><strong>{mergeRequest.state === "merged" ? "Immutable integration evidence" : "Live source review"}:</strong> {summarySourceReviewStatus(mergeRequest)}</p>
{#if mergeRequest.integration_evidence_error !== null}
<p class="workspace-callout is-error"><strong>Integration evidence error:</strong> {mergeRequest.integration_evidence_error}</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>
{#if mergeRequest.state === "merged"}
<p class="workspace-empty-copy">The merged source and integration approval are persisted evidence, not a live selector observation.</p>
{:else}
<p class="workspace-empty-copy">Target-only movement refreshes integration evidence; it does not invalidate approval for an unchanged source.</p>
{/if}
<a
class="workspace-secondary-button"
href={mergeRequestPagePath(data.workspaceId, mergeRequest.merge_request_id)}