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:
@@ -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>
|
||||
|
||||
+5
-2
@@ -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)}
|
||||
|
||||
Reference in New Issue
Block a user