feat(ticket): persist Orchestrator assignment tracking and audit

This commit is contained in:
2026-10-12 02:54:45 +09:00
parent 7f03db9d79
commit 2a55bef036
35 changed files with 7706 additions and 492 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:ba017607e313ba3eb500e4eb78f94c2bd95a08acde96688b8d2a0014abd94ea3)
// Input contract: 0.1.0 (sha256:982cc7edc8fd8655a14e943b9d0022df56e941107def02251186d8ddebfd6774)
// 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:ba017607e313ba3eb500e4eb78f94c2bd95a08acde96688b8d2a0014abd94ea3)
// Input contract: 0.1.0 (sha256:982cc7edc8fd8655a14e943b9d0022df56e941107def02251186d8ddebfd6774)
// 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:ba017607e313ba3eb500e4eb78f94c2bd95a08acde96688b8d2a0014abd94ea3)
// Input contract: 0.1.0 (sha256:982cc7edc8fd8655a14e943b9d0022df56e941107def02251186d8ddebfd6774)
// 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
+23 -3
View File
@@ -1,10 +1,12 @@
// @generated by yoi-ticket-browser-openapi-typescript@1. Do not edit.
// Input: openapi/server-api.json
// Input contract: 0.1.0 (sha256:ba017607e313ba3eb500e4eb78f94c2bd95a08acde96688b8d2a0014abd94ea3)
// Input contract: 0.1.0 (sha256:982cc7edc8fd8655a14e943b9d0022df56e941107def02251186d8ddebfd6774)
// 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
export type Array_of_TicketAssignmentTrackingProjection = Array<TicketAssignmentTrackingProjection>;
export type Array_of_WorkspaceTag = Array<WorkspaceTag>;
export type BrowserAppendTicketEventRequest = { author?: string | null; body: string; role: BrowserTicketThreadRole; };
@@ -87,10 +89,28 @@ export type TicketActionEligibility = { blockers: Array<string>; can_assign_orch
export type TicketAssignmentPrincipal = { account_id: string; kind: "user"; } | { kind: "worker"; runtime_id: string; worker_id: string; } | { agent_key: string; kind: "workspace_agent"; };
export type TicketAssignmentPrincipalSnapshot = { account_id: string; kind: "user"; } | { kind: "worker"; runtime_id: string; worker_id: string; } | { agent_key: string; kind: "workspace_agent"; };
export type TicketAssignmentRelease = { action: TicketAssignmentReleaseAction; actor: string; at: string; event_id: string; last_tracking?: TicketAssignmentTracking | null; operation_key?: string | null; reason?: string | null; replacement_assignment_id?: string | null; };
export type TicketAssignmentReleaseAction = "unassigned" | "replaced";
export type TicketAssignmentRole = "orchestrator" | "worker" | "owner" | "contributor";
export type TicketAssignmentSummary = { assignment_id: string; runtime_id: string; worker_id: string; worker_resource_key?: string | null; };
export type TicketAssignmentTracking = { next_action?: string | null; progress?: string | null; result?: string | null; status: TicketAssignmentTrackingStatus; waiting_reason?: string | null; };
export type TicketAssignmentTrackingHistoryEntry = { at: string; event_id: string; operation_key: string; source_context: unknown; tracking: TicketAssignmentTracking; };
export type TicketAssignmentTrackingHistoryPage = { items: Array<TicketAssignmentTrackingHistoryEntry>; next_event_id?: string | null; };
export type TicketAssignmentTrackingProjection = { assigned_at: string; assigned_by: string; assignment_id: string; current: boolean; last_event_id?: string | null; migration_diagnostic?: string | null; principal: TicketAssignmentPrincipalSnapshot; release?: TicketAssignmentRelease | null; role: string; ticket_id: string; tracking?: TicketAssignmentTracking | null; updated_at?: string | null; workspace_id: string; };
export type TicketAssignmentTrackingStatus = "waiting" | "inprogress" | "done";
export type TicketAssignmentTrackingUpdate = { assignment_id: string; expected_last_event_id: string; operation_key: string; tracking: TicketAssignmentTracking; };
export type TicketAttention = "unresolved_review" | "missing_commit" | "blocked" | "unblocked" | "awaiting_review" | "unresolved_changes" | "stale_after_rescope" | "missing_evidence";
export type TicketCaseAction = { action: "close"; reason: TicketClosureReason; } | { action: "reopen"; } | { action: "correct_closure"; reason: TicketClosureReason; };
@@ -103,7 +123,7 @@ export type TicketClosureJudgment = { at: string; author: string; explanation: s
export type TicketClosureReason = "completed" | "not_planned" | "duplicate" | "unknown" | { other: string; };
export type TicketDetail = { action_eligibility: TicketActionEligibility; artifact_count: number; artifacts: Array<string>; assignment_diagnostics: Array<string>; assignments: Array<TicketRoleAssignmentSummary>; body: string; body_truncated: boolean; closure_history: Array<TicketCaseHistoryEntry>; closure_operation_key?: string | null; content_digest: string; created_at?: string | null; current_closure?: TicketClosureJudgment | null; current_worker?: TicketAssignmentSummary | null; event_count: number; event_page: QueryPage; events: Array<TicketEventDetail>; evidence: TicketEvidenceSummary; id: string; implementation_reports: Array<TicketEvidenceEvent>; legacy_handoff?: TicketLegacyWorkflowSnapshot | null; linked_objectives: Array<ObjectiveLinkSummary>; merge_request?: TicketMergeRequestSummary | null; merge_requests: Array<TicketMergeRequestSummary>; priority: string; queued_at?: string | null; queued_by?: string | null; readiness?: string | null; record_source: string; relations: TicketDetailRelationView; resolution?: string | null; resource_key: string; risk_flags: Array<string>; state: BrowserTicketState; tags: Array<WorkspaceTag>; targets: Array<TicketTarget>; title: string; updated_at?: string | null; };
export type TicketDetail = { action_eligibility: TicketActionEligibility; artifact_count: number; artifacts: Array<string>; assignment_diagnostics: Array<string>; assignment_tracking: Array<TicketAssignmentTrackingProjection>; assignments: Array<TicketRoleAssignmentSummary>; body: string; body_truncated: boolean; closure_history: Array<TicketCaseHistoryEntry>; closure_operation_key?: string | null; content_digest: string; created_at?: string | null; current_closure?: TicketClosureJudgment | null; current_worker?: TicketAssignmentSummary | null; event_count: number; event_page: QueryPage; events: Array<TicketEventDetail>; evidence: TicketEvidenceSummary; id: string; implementation_reports: Array<TicketEvidenceEvent>; legacy_handoff?: TicketLegacyWorkflowSnapshot | null; linked_objectives: Array<ObjectiveLinkSummary>; merge_request?: TicketMergeRequestSummary | null; merge_requests: Array<TicketMergeRequestSummary>; priority: string; queued_at?: string | null; queued_by?: string | null; readiness?: string | null; record_source: string; relations: TicketDetailRelationView; resolution?: string | null; resource_key: string; risk_flags: Array<string>; state: BrowserTicketState; tags: Array<WorkspaceTag>; targets: Array<TicketTarget>; title: string; updated_at?: string | null; };
export type TicketDetailDerivedRelation = { at: string; author: string; forward_kind: string; inverse_kind: string; note?: string | null; source_resource_key?: string | null; source_ticket: string; };
@@ -133,7 +153,7 @@ export type TicketMarkReadyRequest = { intake_summary?: TicketIntakeSummary | nu
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 TicketQueryItem = { blocker_count: number; closure_operation_key?: string | null; content_digest: string; created_at?: string | null; current_closure?: TicketClosureJudgment | null; current_worker?: TicketAssignmentSummary | null; evidence: TicketEvidenceSummary; id: string; legacy_handoff?: TicketLegacyWorkflowSnapshot | null; linked_objective_ids: Array<string>; linked_objective_keys: Array<string>; matched_fields: Array<string>; matching_event?: TicketEvidenceEvent | null; merge_request?: TicketMergeRequestSummary | null; merge_requests: Array<TicketMergeRequestSummary>; priority: string; readiness?: string | null; relation_count: number; resource_key: string; snippet?: string | null; state: BrowserTicketState; tags: Array<WorkspaceTag>; title: string; unresolved_blocker_count: number; unresolved_review_count: number; updated_at?: string | null; workspace_action_priority: string; };
export type TicketQueryItem = { assignment_tracking: Array<TicketAssignmentTrackingProjection>; blocker_count: number; closure_operation_key?: string | null; content_digest: string; created_at?: string | null; current_closure?: TicketClosureJudgment | null; current_worker?: TicketAssignmentSummary | null; evidence: TicketEvidenceSummary; id: string; legacy_handoff?: TicketLegacyWorkflowSnapshot | null; linked_objective_ids: Array<string>; linked_objective_keys: Array<string>; matched_fields: Array<string>; matching_event?: TicketEvidenceEvent | null; merge_request?: TicketMergeRequestSummary | null; merge_requests: Array<TicketMergeRequestSummary>; priority: string; readiness?: string | null; relation_count: number; resource_key: string; snippet?: string | null; state: BrowserTicketState; tags: Array<WorkspaceTag>; title: string; unresolved_blocker_count: number; unresolved_review_count: number; updated_at?: string | null; workspace_action_priority: string; };
export type TicketQueryRequest = { attention?: Array<TicketAttention>; cursor?: string | null; event_kinds?: Array<string>; evidence?: Array<string>; limit?: number & { readonly "__yoiIntegerRange": "0..1000"; } | null; linked_objective_id?: string | null; query?: string | null; related_ticket_id?: string | null; relation_kind?: string | null; review_status?: string | null; sort?: string | null; states?: Array<TicketState>; tag_ids?: Array<string>; tag_ids_any?: Array<string>; tag_ids_none?: Array<string>; updated_after?: string | null; updated_before?: string | null; };
@@ -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:ba017607e313ba3eb500e4eb78f94c2bd95a08acde96688b8d2a0014abd94ea3)
// Input contract: 0.1.0 (sha256:982cc7edc8fd8655a14e943b9d0022df56e941107def02251186d8ddebfd6774)
// 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
@@ -139,6 +139,7 @@ function ticketDetailFixture() {
artifact_count: 0,
artifacts: [],
assignment_diagnostics: [],
assignment_tracking: [],
assignments: [],
closure_history: [],
closure_operation_key: null,
@@ -195,6 +196,52 @@ function ticketDetailFixture() {
};
}
Deno.test("Ticket Browser keeps self-reported delegation independent of case and Worker state", () => {
const tracking = {
workspace_id: "workspace-a",
ticket_id: "ticket-id",
assignment_id: "delegation",
role: "orchestrator",
principal: { kind: "workspace_agent", agent_key: "workspace-orchestrator" },
assigned_by: "workspace-user:alice",
assigned_at: "2026-01-01T00:00:00Z",
release: null,
current: true,
last_event_id: "report-event",
updated_at: null,
migration_diagnostic: null,
tracking: {
status: "done",
waiting_reason: null,
progress: "Investigated",
next_action: "Human decision",
result: "Alternatives returned",
},
};
for (const state of ["open", "closed"]) {
const parsed = parseTicketDetail({
...ticketDetailFixture(),
state,
assignment_tracking: [tracking],
});
assertEquals(parsed.assignment_tracking[0]?.tracking?.status, "done");
assertEquals(parsed.assignment_tracking[0]?.last_event_id, "report-event");
assertEquals(parsed.state, state);
assertEquals(parsed.current_worker, null);
}
assertThrows(
() =>
parseTicketDetail({
...ticketDetailFixture(),
assignment_tracking: [{
...tracking,
tracking: { ...tracking.tracking, status: "running" },
}],
}),
".status is invalid",
);
});
Deno.test("Ticket Browser parser accepts target collections and rejects singular target authority", () => {
const fixture = ticketDetailFixture();
const parsed = parseTicketDetail(fixture);
@@ -20,7 +20,10 @@ import type {
ReviewFinding,
TicketActionEligibility,
TicketAssignmentPrincipal,
TicketAssignmentRelease,
TicketAssignmentSummary,
TicketAssignmentTracking,
TicketAssignmentTrackingProjection,
TicketCaseAction,
TicketCaseHistoryEntry,
TicketClosureJudgment,
@@ -649,6 +652,130 @@ function parseTicketRoleAssignmentRecord(
};
}
function parseAssignmentTracking(
value: unknown,
label: string,
): TicketAssignmentTracking {
const item = object(value, label);
exact(
item,
["status", "waiting_reason", "progress", "next_action", "result"],
label,
);
const status = string(item.status, `${label}.status`);
if (status !== "waiting" && status !== "inprogress" && status !== "done") {
throw new Error(`${label}.status is invalid`);
}
return {
status,
waiting_reason: optionalNullableString(
item.waiting_reason,
`${label}.waiting_reason`,
),
progress: optionalNullableString(item.progress, `${label}.progress`),
next_action: optionalNullableString(
item.next_action,
`${label}.next_action`,
),
result: optionalNullableString(item.result, `${label}.result`),
};
}
function parseAssignmentRelease(
value: unknown,
label: string,
): TicketAssignmentRelease {
const item = object(value, label);
exact(item, [
"action",
"event_id",
"operation_key",
"actor",
"at",
"reason",
"replacement_assignment_id",
"last_tracking",
], label);
const action = string(item.action, `${label}.action`);
if (action !== "unassigned" && action !== "replaced") {
throw new Error(`${label}.action is invalid`);
}
return {
action,
event_id: string(item.event_id, `${label}.event_id`),
operation_key: optionalNullableString(
item.operation_key,
`${label}.operation_key`,
),
actor: string(item.actor, `${label}.actor`),
at: string(item.at, `${label}.at`),
reason: optionalNullableString(item.reason, `${label}.reason`),
replacement_assignment_id: optionalNullableString(
item.replacement_assignment_id,
`${label}.replacement_assignment_id`,
),
last_tracking: item.last_tracking === undefined
? undefined
: item.last_tracking === null
? null
: parseAssignmentTracking(item.last_tracking, `${label}.last_tracking`),
};
}
function parseAssignmentTrackingProjection(
value: unknown,
label: string,
): TicketAssignmentTrackingProjection {
const item = object(value, label);
exact(item, [
"workspace_id",
"ticket_id",
"assignment_id",
"role",
"principal",
"assigned_by",
"assigned_at",
"release",
"current",
"tracking",
"last_event_id",
"updated_at",
"migration_diagnostic",
], label);
return {
workspace_id: string(item.workspace_id, `${label}.workspace_id`),
ticket_id: string(item.ticket_id, `${label}.ticket_id`),
assignment_id: string(item.assignment_id, `${label}.assignment_id`),
role: string(item.role, `${label}.role`),
principal: parseTicketAssignmentPrincipal(
item.principal,
`${label}.principal`,
),
assigned_by: string(item.assigned_by, `${label}.assigned_by`),
assigned_at: string(item.assigned_at, `${label}.assigned_at`),
release: item.release === undefined
? undefined
: item.release === null
? null
: parseAssignmentRelease(item.release, `${label}.release`),
current: boolean(item.current, `${label}.current`),
tracking: item.tracking === undefined
? undefined
: item.tracking === null
? null
: parseAssignmentTracking(item.tracking, `${label}.tracking`),
last_event_id: optionalNullableString(
item.last_event_id,
`${label}.last_event_id`,
),
updated_at: optionalNullableString(item.updated_at, `${label}.updated_at`),
migration_diagnostic: optionalNullableString(
item.migration_diagnostic,
`${label}.migration_diagnostic`,
),
};
}
function parseActionEligibility(
value: unknown,
label: string,
@@ -961,6 +1088,7 @@ export function parseTicketDetail(value: unknown): TicketDetail {
"artifact_count",
"artifacts",
"assignment_diagnostics",
"assignment_tracking",
"assignments",
"closure_history",
"body",
@@ -1012,6 +1140,11 @@ export function parseTicketDetail(value: unknown): TicketDetail {
item.assignment_diagnostics,
`${label}.assignment_diagnostics`,
),
assignment_tracking: array(
item.assignment_tracking,
`${label}.assignment_tracking`,
parseAssignmentTrackingProjection,
),
assignments: array(
item.assignments,
`${label}.assignments`,