feat: generate ticket browser contract from ServerApi
This commit is contained in:
@@ -6,12 +6,14 @@
|
||||
"dev": "deno run -A npm:vite@7.2.7 dev",
|
||||
"dev:backend": "cd ../.. && cargo run -p yoi-workspace-server --bin yoi-server -- serve --listen 127.0.0.1:8787",
|
||||
"check": "deno run -A npm:@sveltejs/kit@2.49.4 sync && deno run -A npm:svelte-check@4.3.4 --tsconfig ./tsconfig.json",
|
||||
"test": "deno test --allow-read=src,test,tests,../../docs --allow-env=LOG,VSCODE_TEXTMATE_DEBUG,NODE_ENV tests/workspace-model.test.ts tests/repository-api.test.ts tests/workspace-catalog.test.ts tests/profile-api.test.ts tests/skill-api.test.ts tests/tooltip-contract.test.ts tests/bevel-contract.test.ts tests/bevel-line-contract.test.ts tests/typography-contract.test.ts src/lib/workspace/auth/model.test.ts tests/auth-api.test.ts src/lib/workspace/api/http.test.ts src/lib/workspace/api/workers.test.ts src/lib/workspace/header/breadcrumb-model.test.ts src/lib/workspace/console/chat-submit.test.ts src/lib/workspace/console/composer-delivery.test.ts test/composer-history.test.ts tests/composer-paste.test.ts src/lib/workspace/console/composer-command.test.ts src/lib/workspace/console/composer-draft.test.ts src/lib/workspace/console/composer-completion.test.ts test/console/ansi.test.ts src/lib/workspace/console/model.test.ts src/lib/workspace/console/run-status.test.ts src/lib/workspace/companion/api.test.ts tests/workdir-api.test.ts src/lib/workspace/console/tasks.test.ts test/ticket-detail-route-reuse.test.ts test/objective-pages.test.ts test/repositories/ui.test.ts src/lib/workspace/console/worker-console.ui.test.ts src/lib/workspace/settings/model.test.ts tests/runtime-connection.test.ts tests/runtime-management.test.ts tests/runtime-management-source.test.ts src/lib/workspace/sidebar/override-stack.test.ts src/lib/workspace/sidebar/workers.test.ts src/lib/workspace/sidebar/workspace-switcher.test.ts src/lib/workspace/sidebar/worker-subscription.test.ts src/lib/workspace/sidebar/worker-workdir-meta.test.ts src/lib/workspace/sidebar/worker-launch.test.ts test/sidebar/worker-actions.test.ts src/lib/workspace/tickets/merge-request-resources.test.ts src/lib/workspace/tickets/ticket-panel.test.ts test/merge-request-status.test.ts test/config-source/decodal-grammar.test.ts test/config-source/editor-state.test.ts test/config-source/fixed-schema-wrapper.test.ts test/config-source/toolchain.test.ts test/config-source/wasm-parity.test.ts test/config-source/api.test.ts tests/memory-api.test.ts test/repository-access/api.test.ts test/repository-access/loader.test.ts test/repository-access/ui.test.ts test/repository-ssh-connection-ui.test.ts",
|
||||
"test": "deno test --allow-read=src,test,tests,../../docs --allow-env=LOG,VSCODE_TEXTMATE_DEBUG,NODE_ENV tests/workspace-model.test.ts tests/repository-api.test.ts tests/workspace-catalog.test.ts tests/profile-api.test.ts tests/skill-api.test.ts tests/tooltip-contract.test.ts tests/bevel-contract.test.ts tests/bevel-line-contract.test.ts tests/typography-contract.test.ts src/lib/workspace/auth/model.test.ts tests/auth-api.test.ts src/lib/workspace/api/http.test.ts src/lib/workspace/api/workers.test.ts src/lib/workspace/api/ticket-browser.test.ts src/lib/workspace/header/breadcrumb-model.test.ts src/lib/workspace/console/chat-submit.test.ts src/lib/workspace/console/composer-delivery.test.ts test/composer-history.test.ts tests/composer-paste.test.ts src/lib/workspace/console/composer-command.test.ts src/lib/workspace/console/composer-draft.test.ts src/lib/workspace/console/composer-completion.test.ts test/console/ansi.test.ts src/lib/workspace/console/model.test.ts src/lib/workspace/console/run-status.test.ts src/lib/workspace/companion/api.test.ts tests/workdir-api.test.ts src/lib/workspace/console/tasks.test.ts test/ticket-detail-route-reuse.test.ts test/objective-pages.test.ts test/repositories/ui.test.ts src/lib/workspace/console/worker-console.ui.test.ts src/lib/workspace/settings/model.test.ts tests/runtime-connection.test.ts tests/runtime-management.test.ts tests/runtime-management-source.test.ts src/lib/workspace/sidebar/override-stack.test.ts src/lib/workspace/sidebar/workers.test.ts src/lib/workspace/sidebar/workspace-switcher.test.ts src/lib/workspace/sidebar/worker-subscription.test.ts src/lib/workspace/sidebar/worker-workdir-meta.test.ts src/lib/workspace/sidebar/worker-launch.test.ts test/sidebar/worker-actions.test.ts src/lib/workspace/tickets/merge-request-resources.test.ts src/lib/workspace/tickets/ticket-panel.test.ts test/merge-request-status.test.ts test/config-source/decodal-grammar.test.ts test/config-source/editor-state.test.ts test/config-source/fixed-schema-wrapper.test.ts test/config-source/toolchain.test.ts test/config-source/wasm-parity.test.ts test/config-source/api.test.ts tests/memory-api.test.ts test/repository-access/api.test.ts test/repository-access/loader.test.ts test/repository-access/ui.test.ts test/repository-ssh-connection-ui.test.ts",
|
||||
"test:component": "deno run -A npm:vitest@3.2.4 run --config vitest.config.ts",
|
||||
"build": "deno run -A npm:vite@7.2.7 build",
|
||||
"preview": "deno run -A npm:vite@7.2.7 preview",
|
||||
"generate:repository-api": "cd ../.. && cargo run -q -p server-api --example generate_repository_openapi_types",
|
||||
"check:repository-api": "cd ../.. && cargo run -q -p server-api --example generate_repository_openapi_types -- --check"
|
||||
"check:repository-api": "cd ../.. && cargo run -q -p server-api --example generate_repository_openapi_types -- --check",
|
||||
"generate:ticket-api": "cd ../.. && cargo run -q -p server-api --example generate_ticket_api_types",
|
||||
"check:ticket-api": "cd ../.. && cargo run -q -p server-api --example generate_ticket_api_types -- --check"
|
||||
},
|
||||
"imports": {
|
||||
"$lib/": "./src/lib/",
|
||||
|
||||
@@ -1,54 +1,126 @@
|
||||
// Generated from yoi-workspace-server. Do not edit by hand.
|
||||
// Regenerate: cargo run -q -p yoi-workspace-server --features typescript --example generate_ticket_api_types > web/workspace/src/lib/generated/ticket-api.ts
|
||||
// @generated by yoi-ticket-browser-openapi-typescript@1. Do not edit.
|
||||
// Input: openapi/server-api.json
|
||||
// Input contract: 0.1.0 (sha256:550383b600fe2a2e55630849c7de7a6a24463ad41d0c18dc5d254a3d14328f07)
|
||||
// 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 InvalidProjectRecord = { label: string, reason: string, };
|
||||
export type BrowserAppendTicketEventRequest = { author?: string | null; body: string; role: BrowserTicketThreadRole; };
|
||||
|
||||
export type TicketSummary = { id: string, resource_key: string, title: string, state: string, priority: string, updated_at: string | null, queued_by: string | null, queued_at: string | null, workspace_action_priority: string, record_source: string, };
|
||||
export type BrowserCloseTicketRequest = { resolution: string; };
|
||||
|
||||
export type TicketListResponse = { workspace_id: string, limit: number, items: Array<TicketSummary>, page: QueryPage, invalid_records: Array<InvalidProjectRecord>, record_authority: string, };
|
||||
export type BrowserEditTicketRequest = { author?: string | null; body?: string | null; new_string?: string | null; old_string?: string | null; replace_all?: boolean; target?: BrowserTicketTargetEdit | null; title?: string | null; };
|
||||
|
||||
export type QueryPage = { limit: number, returned: number, has_more: boolean, next_cursor: string | null, sort: string, source_limit: number | null, source_truncated: boolean, };
|
||||
export type BrowserQueueTicketRequest = { [key: string]: never; };
|
||||
|
||||
export type TicketEventDetail = { sequence: number, event_ref: string, kind: string, author: string | null, at: string | null, status: string | null, from: string | null, to: string | null, reason: string | null, state_field: string | null, heading: string | null, body: string | null, attributes: { [key in string]: string }, references: Array<string>, };
|
||||
export type BrowserTicketTargetEdit = { action: "set"; ref_selector?: string | null; repository_key: string; } | { action: "clear"; };
|
||||
|
||||
export type ObjectiveLinkSummary = { id: string, resource_key: string, title: string, state: string, };
|
||||
export type BrowserTicketThreadRole = "comment" | "plan" | "decision" | "implementation_report";
|
||||
|
||||
export type TicketEvidenceEvent = { event_ref: string, sequence: number, kind: string, at: string | null, author: string | null, excerpt: string, };
|
||||
export type BrowserTicketWorkflowState = "planning" | "ready" | "queued" | "inprogress" | "done" | "closed";
|
||||
|
||||
export type TicketAssignmentSummary = { assignment_id: string, runtime_id: string, worker_id: string, worker_resource_key?: string | null, };
|
||||
export type BrowserTransitionTicketStateRequest = { author?: string | null; body?: string | null; reason?: string | null; state: BrowserTicketWorkflowState; };
|
||||
|
||||
export type TicketRoleAssignmentSummary = { assignment_id: string, role: string, principal: TicketAssignmentPrincipalSummary, assigned_by: string, assigned_at: string, };
|
||||
export type CancelTicketImplementationRequest = { assignment_id: string; operation_id: string; reason: string; };
|
||||
|
||||
export type TicketAssignmentPrincipalSummary = { "kind": "user", account_id: string, } | { "kind": "worker", runtime_id: string, worker_id: string, } | { "kind": "workspace_agent", agent_key: string, };
|
||||
export type ConflictResolution = "none" | "clean" | "conflicts_resolved";
|
||||
|
||||
export type TicketActionEligibility = { can_assign_orchestrator: boolean, can_unassign_orchestrator: boolean, can_queue: boolean, can_start_manual_coder: boolean, queue_tickets: Array<string>, blockers: Array<string>, };
|
||||
export type Diagnostic = { code: string; message: string; severity: DiagnosticSeverity; };
|
||||
|
||||
export type TicketMergeRequestSummary = { merge_request_id: string, repository_key: string, state: string, review_status: string, selector_from: string | null, selector_to: string, updated_at: string, current_subject_ref: string | null, review_subject_ref: string | null, review_requested_at: string | null, review_submitted_at: string | null, review_excerpt: string | null, };
|
||||
export type DiagnosticSeverity = "info" | "warning" | "error";
|
||||
|
||||
export type MergeRequestRefDiagnostic = { code: string, message: string, };
|
||||
export type FindingSeverity = "blocker" | "major" | "minor" | "note";
|
||||
|
||||
export type MergeRequestListItem = { summary: TicketMergeRequestSummary, ticket_ids: Array<string>, thread_event_count: number, ref_diagnostics?: Array<MergeRequestRefDiagnostic>, };
|
||||
export type InvalidProjectRecord = { label: string; reason: string; };
|
||||
|
||||
export type MergeRequestListResponse = { items: Array<MergeRequestListItem>, next_cursor: string | null, };
|
||||
export type MarkdownText = string;
|
||||
|
||||
export type TicketEvidenceSummary = { has_merge_request: boolean, has_current_subject_ref: boolean, has_review_request: boolean, has_commit: boolean, review_status: string | null, approved_current_subject: boolean, review_after_rescope: boolean, unresolved_request_changes: boolean, complete_for_integration: boolean, missing: Array<string>, };
|
||||
export type MergeRequestDetailResponse = { created_at: string; linked_tickets: Array<MergeRequestLinkedTicketResponse>; merge_request_id: string; repository_key: string; selector_from?: string | null; selector_to: string; source: MergeRequestRefResponse; state: MergeRequestState; target: MergeRequestRefResponse; thread: Array<MergeRequestThreadEvent>; ticket_ids: Array<string>; updated_at: string; workspace_id: string; };
|
||||
|
||||
export type TicketQueryRequest = { query: string | null, states: Array<string>, event_kinds: Array<string>, evidence: Array<string>, review_status: string | null, attention: Array<string>, related_ticket_id: string | null, relation_kind: string | null, linked_objective_id: string | null, updated_after: string | null, updated_before: string | null, sort: string | null, limit: number | null, cursor: string | null, };
|
||||
export type MergeRequestLinkedTicketResponse = { key?: string | null; ticket_id: string; };
|
||||
|
||||
export type TicketQueryItem = { id: string, resource_key: string, title: string, state: string, readiness: string | null, priority: string, created_at: string | null, updated_at: string | null, item_revision: string, workspace_action_priority: string, matched_fields: Array<string>, snippet: string | null, matching_event: TicketEvidenceEvent | null, linked_objective_ids: Array<string>, relation_count: number, blocker_count: number, unresolved_blocker_count: number, unresolved_review_count: number, evidence: TicketEvidenceSummary, merge_request: TicketMergeRequestSummary | null, };
|
||||
export type MergeRequestListItem = { ref_diagnostics?: Array<MergeRequestRefDiagnostic>; summary: TicketMergeRequestSummary; thread_event_count: number; ticket_ids: Array<string>; };
|
||||
|
||||
export type TicketQueryResponse = { items: Array<TicketQueryItem>, page: QueryPage, record_authority: string, };
|
||||
export type MergeRequestListResponse = { items: Array<MergeRequestListItem>; next_cursor?: string | null; };
|
||||
|
||||
export type TicketShowRequest = { event_limit: number | null, event_cursor: string | null, };
|
||||
export type MergeRequestRefDiagnostic = { code: string; message: string; };
|
||||
|
||||
export type TicketRelation = { ticket_id: string, kind: string, target: string, target_resource_key?: string | null, note: string | null, author: string, at: string, };
|
||||
export type MergeRequestRefResponse = { diagnostic?: MergeRequestRefDiagnostic | null; observed_at: string; ref?: string | null; status: string; };
|
||||
|
||||
export type DerivedTicketRelation = { source_ticket: string, source_resource_key?: string | null, inverse_kind: string, forward_kind: string, note: string | null, author: string, at: string, };
|
||||
export type MergeRequestState = "open" | "merged" | "closed";
|
||||
|
||||
export type TicketRelationBlocker = { blocking_ticket: string, blocking_resource_key?: string | null, reason_kind: string, relation_kind: string, note: string | null, blocking_state: string, };
|
||||
export type MergeRequestThreadEvent = { created_at: string; event_id: string; kind: "review_requested"; requested_by: MergeRequestWorkerIdentity; reviewer: MergeRequestWorkerIdentity; sequence: number; subject_ref: string; } | { body: string; created_at: string; decision: ReviewDecision; event_id: string; findings: Array<ReviewFinding>; kind: "review"; request_event_id: string; reviewer: MergeRequestWorkerIdentity; sequence: number; subject_ref: string; } | { created_at: string; event_id: string; kind: "review_revoked"; reason: string; review_event_id: string; revoked_by: MergeRequestWorkerIdentity; sequence: number; subject_ref: string; } | { created_at: string; event_id: string; kind: "review_cancelled"; reason: string; request_event_id: string; sequence: number; subject_ref: string; } | { author: MergeRequestWorkerIdentity; body: string; created_at: string; event_id: string; kind: "comment"; sequence: number; } | { approval_event_id: string; approved_source_ref: string; created_at: string; event_id: string; kind: "merge"; merged_by: MergeRequestWorkerIdentity; operation_id: string; resolution: ConflictResolution; sequence: number; strategy: MergeStrategy; target_ref_after: string; target_ref_before: string; };
|
||||
|
||||
export type TicketRelationNotice = { related_ticket: string, kind: string, message: string, };
|
||||
export type MergeRequestWorkerIdentity = { runtime_id: string; worker_id: string; };
|
||||
|
||||
export type TicketRelationView = { outgoing: Array<TicketRelation>, incoming: Array<DerivedTicketRelation>, blockers: Array<TicketRelationBlocker>, notices: Array<TicketRelationNotice>, };
|
||||
export type MergeStrategy = "fast_forward" | "merge";
|
||||
|
||||
export type TicketDetail = { id: string, resource_key: string, title: string, state: string, readiness: string | null, priority: string, created_at: string | null, updated_at: string | null, item_revision: string, queued_by: string | null, queued_at: string | null, repository_key: string | null, ref_selector: string | null, risk_flags: Array<string>, body: string, body_truncated: boolean, event_count: number, events: Array<TicketEventDetail>, event_page: QueryPage, artifact_count: number, artifacts: Array<string>, relations: TicketRelationView, linked_objectives: Array<ObjectiveLinkSummary>, implementation_reports: Array<TicketEvidenceEvent>, assignments: Array<TicketRoleAssignmentSummary>, current_coder: TicketAssignmentSummary | null, assignment_diagnostics: Array<string>, action_eligibility: TicketActionEligibility, merge_request: TicketMergeRequestSummary | null, evidence: TicketEvidenceSummary, resolution: string | null, record_source: string, };
|
||||
export type ObjectiveDetail = { body: string; body_truncated: boolean; created_at?: string | null; event_page: QueryPage; events: Array<ObjectiveEventDetail>; id: string; linked_ticket_summaries: Array<ObjectiveLinkedTicketSummary>; linked_tickets: Array<string>; record_source: string; resource_key: string; resources: Array<ObjectiveResourceSummary>; revision: string; state: string; title: string; updated_at?: string | null; };
|
||||
|
||||
export type ObjectiveEventDetail = { body?: string | null; created_at: string; event_ref: string; kind: string; };
|
||||
|
||||
export type ObjectiveLinkSummary = { id: string; resource_key: string; state: string; title: string; };
|
||||
|
||||
export type ObjectiveLinkedTicketSummary = { id: string; resource_key: string; state: string; title: string; };
|
||||
|
||||
export type ObjectiveListResponse = { invalid_records: Array<InvalidProjectRecord>; items: Array<ObjectiveSummary>; limit: number & { readonly "__yoiIntegerRange": "0..1000"; }; record_authority: string; workspace_id: string; };
|
||||
|
||||
export type ObjectiveResourceSummary = { bytes: number; media_type?: string | null; path: string; updated_at: string; };
|
||||
|
||||
export type ObjectiveSummary = { created_at?: string | null; id: string; linked_tickets: Array<string>; record_source: string; resource_key: string; state: string; summary: string; title: string; updated_at?: string | null; };
|
||||
|
||||
export type QueryPage = { has_more: boolean; limit: number; next_cursor?: string | null; returned: number; sort: string; source_limit?: number | null; source_truncated: boolean; };
|
||||
|
||||
export type RepositoryApiError = { diagnostics?: Array<Diagnostic>; error: string; message: string; };
|
||||
|
||||
export type ReviewDecision = "approve" | "request_changes";
|
||||
|
||||
export type ReviewFinding = { body: string; code?: string | null; line?: number | null; path?: string | null; severity: FindingSeverity; };
|
||||
|
||||
export type SetTicketRoleAssignmentRequest = { expected_assignment_id?: string | null; operation_id: string; principal: TicketAssignmentPrincipal; };
|
||||
|
||||
export type TicketActionEligibility = { blockers: Array<string>; can_assign_orchestrator: boolean; can_queue: boolean; can_start_manual_coder: boolean; can_unassign_orchestrator: boolean; queue_tickets: Array<string>; };
|
||||
|
||||
export type TicketAssignmentPrincipal = { account_id: string; kind: "user"; } | { kind: "worker"; runtime_id: string; worker_id: string; } | { agent_key: string; kind: "workspace_agent"; };
|
||||
|
||||
export type TicketAssignmentRole = "orchestrator" | "coder" | "owner" | "contributor";
|
||||
|
||||
export type TicketAssignmentSummary = { assignment_id: string; runtime_id: string; worker_id: string; worker_resource_key?: string | null; };
|
||||
|
||||
export type TicketDetail = { action_eligibility: TicketActionEligibility; artifact_count: number; artifacts: Array<string>; assignment_diagnostics: Array<string>; assignments: Array<TicketRoleAssignmentSummary>; body: string; body_truncated: boolean; created_at?: string | null; current_coder?: TicketAssignmentSummary | null; event_count: number; event_page: QueryPage; events: Array<TicketEventDetail>; evidence: TicketEvidenceSummary; id: string; implementation_reports: Array<TicketEvidenceEvent>; item_revision: string; linked_objectives: Array<ObjectiveLinkSummary>; merge_request?: TicketMergeRequestSummary | null; priority: string; queued_at?: string | null; queued_by?: string | null; readiness?: string | null; record_source: string; ref_selector?: string | null; relations: TicketDetailRelationView; repository_key?: string | null; resolution?: string | null; resource_key: string; risk_flags: Array<string>; state: string; 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; };
|
||||
|
||||
export type TicketDetailRelation = { at: string; author: string; kind: string; note?: string | null; target: string; target_resource_key?: string | null; ticket_id: string; };
|
||||
|
||||
export type TicketDetailRelationBlocker = { blocking_resource_key?: string | null; blocking_state: string; blocking_ticket: string; note?: string | null; reason_kind: string; relation_kind: string; };
|
||||
|
||||
export type TicketDetailRelationNotice = { kind: string; message: string; related_ticket: string; };
|
||||
|
||||
export type TicketDetailRelationView = { blockers: Array<TicketDetailRelationBlocker>; incoming: Array<TicketDetailDerivedRelation>; notices: Array<TicketDetailRelationNotice>; outgoing: Array<TicketDetailRelation>; };
|
||||
|
||||
export type TicketEventDetail = { at?: string | null; attributes: { [key: string]: string; }; author?: string | null; body?: string | null; event_ref: string; from?: string | null; heading?: string | null; kind: string; reason?: string | null; references: Array<string>; sequence: number; state_field?: string | null; status?: string | null; to?: string | null; };
|
||||
|
||||
export type TicketEvidenceEvent = { at?: string | null; author?: string | null; event_ref: string; excerpt: string; kind: string; sequence: number; };
|
||||
|
||||
export type TicketEvidenceSummary = { approved_current_subject: boolean; complete_for_integration: boolean; has_commit: boolean; has_current_subject_ref: boolean; has_merge_request: boolean; has_review_request: boolean; missing: Array<string>; review_after_rescope: boolean; review_status?: string | null; unresolved_request_changes: boolean; };
|
||||
|
||||
export type TicketIntakeSummary = { author?: string | null; body: MarkdownText; references: Array<TicketReference>; };
|
||||
|
||||
export type TicketListItemSummary = { id: string; priority: string; queued_at?: string | null; queued_by?: string | null; record_source: string; resource_key: string; state: string; title: string; updated_at?: string | null; workspace_action_priority: string; };
|
||||
|
||||
export type TicketListResponse = { invalid_records: Array<InvalidProjectRecord>; items: Array<TicketListItemSummary>; limit: number & { readonly "__yoiIntegerRange": "0..1000"; }; page: QueryPage; record_authority: string; workspace_id: string; };
|
||||
|
||||
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 TicketQueueOutcome = { queued_tickets: Array<string>; requested_ticket: string; };
|
||||
|
||||
export type TicketReference = { kind: string; target: string; };
|
||||
|
||||
export type TicketRoleAssignmentMutationResponse = { assignment?: TicketRoleAssignmentRecord | null; ticket_id: string; workspace_id: string; };
|
||||
|
||||
export type TicketRoleAssignmentRecord = { assigned_at: string; assigned_by: string; assignment_id: string; principal: TicketAssignmentPrincipal; role: TicketAssignmentRole; ticket_id: string; workspace_id: string; };
|
||||
|
||||
export type TicketRoleAssignmentSummary = { assigned_at: string; assigned_by: string; assignment_id: string; principal: TicketAssignmentPrincipal; role: string; };
|
||||
|
||||
@@ -177,23 +177,41 @@ export async function readBoundedJson(
|
||||
return JSON.parse(text) as unknown;
|
||||
}
|
||||
|
||||
async function requireJson<T>(response: Response, path: string): Promise<T> {
|
||||
async function requireJson<T>(
|
||||
response: Response,
|
||||
path: string,
|
||||
parse: (value: unknown) => T,
|
||||
maxResponseBytes?: number,
|
||||
): Promise<T> {
|
||||
if (!response.ok) {
|
||||
if (maxResponseBytes !== undefined) {
|
||||
await response.body?.cancel();
|
||||
throw new Error(`${path} request failed (${response.status})`);
|
||||
}
|
||||
const text = await response.text();
|
||||
throw new Error(text || `${path} request failed (${response.status})`);
|
||||
}
|
||||
return (await response.json()) as T;
|
||||
const value: unknown = maxResponseBytes === undefined
|
||||
? await response.json()
|
||||
: await readBoundedJson(response, maxResponseBytes);
|
||||
return parse(value);
|
||||
}
|
||||
|
||||
export async function workspaceApiJson<T>(path: string): Promise<T> {
|
||||
return requireJson<T>(await fetch(path), path);
|
||||
export async function workspaceApiJson<T>(
|
||||
path: string,
|
||||
parse: (value: unknown) => T = (value) => value as T,
|
||||
maxResponseBytes?: number,
|
||||
): Promise<T> {
|
||||
return requireJson(await fetch(path), path, parse, maxResponseBytes);
|
||||
}
|
||||
|
||||
export async function workspaceApiJsonWithBody<T>(
|
||||
path: string,
|
||||
init: RequestInit,
|
||||
parse: (value: unknown) => T = (value) => value as T,
|
||||
maxResponseBytes?: number,
|
||||
): Promise<T> {
|
||||
return requireJson<T>(
|
||||
return requireJson(
|
||||
await fetch(path, {
|
||||
headers: {
|
||||
"content-type": "application/json",
|
||||
@@ -202,6 +220,8 @@ export async function workspaceApiJsonWithBody<T>(
|
||||
...init,
|
||||
}),
|
||||
path,
|
||||
parse,
|
||||
maxResponseBytes,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,48 +1,13 @@
|
||||
import type { MergeRequestListResponse } from "$lib/generated/ticket-api";
|
||||
import type {
|
||||
MergeRequestDetailResponse,
|
||||
MergeRequestListResponse,
|
||||
MergeRequestThreadEvent,
|
||||
} from "$lib/generated/ticket-api";
|
||||
import { workspaceApiPath } from "./http";
|
||||
|
||||
export type MergeRequestState = "open" | "merged" | "closed";
|
||||
|
||||
export type MergeRequestActor = {
|
||||
runtime_id: string;
|
||||
worker_id: string;
|
||||
assignment_id: string;
|
||||
};
|
||||
|
||||
export type MergeRequestThreadEvent = {
|
||||
kind: string;
|
||||
sequence: number;
|
||||
at: string;
|
||||
[key: string]: unknown;
|
||||
};
|
||||
|
||||
export type MergeRequestRecord = {
|
||||
merge_request_id: string;
|
||||
workspace_id: string;
|
||||
repository_key: string;
|
||||
selector_from: string | null;
|
||||
selector_to: string;
|
||||
ticket_ids: string[];
|
||||
state: MergeRequestState;
|
||||
opened_by: MergeRequestActor;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
thread: MergeRequestThreadEvent[];
|
||||
};
|
||||
|
||||
export type MergeRequestRefObservation = {
|
||||
status: string;
|
||||
ref: string | null;
|
||||
observed_at: string;
|
||||
};
|
||||
|
||||
export type MergeRequestDetail = MergeRequestRecord & {
|
||||
source: MergeRequestRefObservation;
|
||||
target: MergeRequestRefObservation;
|
||||
linked_tickets: Array<{ ticket_id: string; key: string | null }>;
|
||||
};
|
||||
|
||||
export type MergeRequestDetail = MergeRequestDetailResponse;
|
||||
export type MergeRequestListPage = MergeRequestListResponse;
|
||||
export type { MergeRequestThreadEvent };
|
||||
|
||||
export function mergeRequestCollectionPath(workspaceId: string): string {
|
||||
return workspaceApiPath(workspaceId, "/merge-requests");
|
||||
|
||||
@@ -0,0 +1,143 @@
|
||||
declare const Deno: {
|
||||
test(name: string, fn: () => Promise<void> | void): void;
|
||||
};
|
||||
|
||||
import {
|
||||
parseMergeRequestDetailResponse,
|
||||
parseObjectiveListResponse,
|
||||
parseTicketListResponse,
|
||||
} from "./ticket-browser.ts";
|
||||
|
||||
function assertEquals(actual: unknown, expected: unknown): void {
|
||||
const actualJson = JSON.stringify(actual);
|
||||
const expectedJson = JSON.stringify(expected);
|
||||
if (actualJson !== expectedJson) {
|
||||
throw new Error(`Expected ${expectedJson}, received ${actualJson}`);
|
||||
}
|
||||
}
|
||||
|
||||
function assertThrows(operation: () => unknown, message: string): void {
|
||||
try {
|
||||
operation();
|
||||
} catch (error) {
|
||||
if (error instanceof Error && error.message.includes(message)) return;
|
||||
throw error;
|
||||
}
|
||||
throw new Error(`Expected operation to throw: ${message}`);
|
||||
}
|
||||
|
||||
const page = {
|
||||
has_more: false,
|
||||
limit: 30,
|
||||
next_cursor: null,
|
||||
returned: 1,
|
||||
sort: "updated_desc",
|
||||
source_limit: null,
|
||||
source_truncated: false,
|
||||
};
|
||||
|
||||
Deno.test("Ticket Browser parser accepts the generated Ticket list shape", () => {
|
||||
const parsed = parseTicketListResponse({
|
||||
workspace_id: "workspace-a",
|
||||
limit: 30,
|
||||
items: [{
|
||||
id: "ticket-id",
|
||||
resource_key: "T-1",
|
||||
title: "Ticket",
|
||||
state: "ready",
|
||||
priority: "P2",
|
||||
updated_at: null,
|
||||
queued_by: null,
|
||||
queued_at: null,
|
||||
workspace_action_priority: "ready",
|
||||
record_source: "sqlite",
|
||||
}],
|
||||
page,
|
||||
invalid_records: [],
|
||||
record_authority: "sqlite",
|
||||
});
|
||||
assertEquals(parsed.items[0]?.resource_key, "T-1");
|
||||
});
|
||||
|
||||
Deno.test("Ticket Browser parser rejects unknown fields and out-of-range limits", () => {
|
||||
const fixture = {
|
||||
workspace_id: "workspace-a",
|
||||
limit: 30,
|
||||
items: [],
|
||||
page: { ...page, returned: 0 },
|
||||
invalid_records: [],
|
||||
record_authority: "sqlite",
|
||||
};
|
||||
assertThrows(
|
||||
() => parseTicketListResponse({ ...fixture, stale: true }),
|
||||
"unknown field stale",
|
||||
);
|
||||
assertThrows(
|
||||
() => parseTicketListResponse({ ...fixture, limit: 1001 }),
|
||||
"0 through 1000",
|
||||
);
|
||||
});
|
||||
|
||||
Deno.test("Ticket Browser parser accepts the generated Objective list shape", () => {
|
||||
const parsed = parseObjectiveListResponse({
|
||||
workspace_id: "workspace-a",
|
||||
limit: 100,
|
||||
items: [{
|
||||
id: "objective-id",
|
||||
resource_key: "O-1",
|
||||
title: "Objective",
|
||||
state: "active",
|
||||
created_at: null,
|
||||
updated_at: null,
|
||||
summary: "Summary",
|
||||
linked_tickets: ["T-1"],
|
||||
record_source: "sqlite",
|
||||
}],
|
||||
invalid_records: [],
|
||||
record_authority: "sqlite",
|
||||
});
|
||||
assertEquals(parsed.items[0]?.resource_key, "O-1");
|
||||
});
|
||||
|
||||
Deno.test("Ticket Browser parser validates the canonical Merge Request thread", () => {
|
||||
const parsed = parseMergeRequestDetailResponse({
|
||||
workspace_id: "workspace-a",
|
||||
merge_request_id: "mr-1",
|
||||
repository_key: "main",
|
||||
state: "open",
|
||||
selector_from: "work/T-1",
|
||||
selector_to: "develop",
|
||||
ticket_ids: ["ticket-id"],
|
||||
created_at: "2026-09-22T00:00:00Z",
|
||||
updated_at: "2026-09-22T00:00:00Z",
|
||||
thread: [{
|
||||
kind: "review_requested",
|
||||
event_id: "event-1",
|
||||
sequence: 1,
|
||||
subject_ref: "abc123",
|
||||
requested_by: { runtime_id: "runtime-a", worker_id: "worker-a" },
|
||||
reviewer: { runtime_id: "runtime-a", worker_id: "reviewer-a" },
|
||||
created_at: "2026-09-22T00:00:00Z",
|
||||
}],
|
||||
source: {
|
||||
status: "resolved",
|
||||
ref: "abc123",
|
||||
observed_at: "2026-09-22T00:00:00Z",
|
||||
},
|
||||
target: {
|
||||
status: "resolved",
|
||||
ref: "def456",
|
||||
observed_at: "2026-09-22T00:00:00Z",
|
||||
},
|
||||
linked_tickets: [{ ticket_id: "ticket-id", key: "T-1" }],
|
||||
});
|
||||
assertEquals(parsed.thread[0]?.kind, "review_requested");
|
||||
assertThrows(
|
||||
() =>
|
||||
parseMergeRequestDetailResponse({
|
||||
...parsed,
|
||||
thread: [{ ...parsed.thread[0], unexpected: true }],
|
||||
}),
|
||||
"unknown field unexpected",
|
||||
);
|
||||
});
|
||||
File diff suppressed because it is too large
Load Diff
@@ -385,7 +385,7 @@ Deno.test("workspace Tickets surface provides Kanban and lifecycle controls", as
|
||||
"Tickets, Objectives, and Merge Requests should each be a single primary sidebar link",
|
||||
);
|
||||
assert(
|
||||
ticketsLoad.includes("Object.entries(LANE_STATES)") &&
|
||||
ticketsLoad.includes("loadLane(fetch, workspaceId") &&
|
||||
ticketsLoad.includes('limit: "30"') &&
|
||||
ticketsLoad.includes('states: states.join(",")') &&
|
||||
ticketsLoad.includes("/tickets?${search}") &&
|
||||
@@ -405,9 +405,12 @@ Deno.test("workspace Tickets surface provides Kanban and lifecycle controls", as
|
||||
"Ticket Kanban should combine related states into independent 30-item display windows",
|
||||
);
|
||||
assert(
|
||||
generatedTicketApi.includes("Generated from yoi-workspace-server") &&
|
||||
generatedTicketApi.includes(
|
||||
"@generated by yoi-ticket-browser-openapi-typescript@1",
|
||||
) &&
|
||||
generatedTicketApi.includes("Input contract: 0.1.0 (sha256:") &&
|
||||
generatedTicketApi.includes("export type TicketListResponse") &&
|
||||
generatedTicketApi.includes("items: Array<TicketSummary>"),
|
||||
generatedTicketApi.includes("items: Array<TicketListItemSummary>"),
|
||||
"Ticket response types should come from the generated Server contract",
|
||||
);
|
||||
assert(
|
||||
|
||||
@@ -3,11 +3,17 @@ import type {
|
||||
MergeRequestThreadEvent,
|
||||
} from "./api/merge-requests.ts";
|
||||
|
||||
type ReviewEvent = Extract<MergeRequestThreadEvent, { kind: "review" }>;
|
||||
type SourceEvidenceEvent = Extract<
|
||||
MergeRequestThreadEvent,
|
||||
{ kind: "review" | "review_requested" }
|
||||
>;
|
||||
|
||||
function isCurrentSourceEvent(
|
||||
event: MergeRequestThreadEvent,
|
||||
source: string,
|
||||
): boolean {
|
||||
return event.subject_ref === source;
|
||||
return "subject_ref" in event && event.subject_ref === source;
|
||||
}
|
||||
|
||||
function requestHasTerminalOutcome(
|
||||
@@ -26,16 +32,18 @@ export function sourceReviewFreshness(
|
||||
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,
|
||||
);
|
||||
});
|
||||
const effectiveReview = [...mergeRequest.thread].reverse().find(
|
||||
(event): event is ReviewEvent => {
|
||||
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}.`
|
||||
@@ -43,9 +51,8 @@ export function sourceReviewFreshness(
|
||||
}
|
||||
|
||||
const latestEvidence = [...mergeRequest.thread].reverse().find(
|
||||
(event) =>
|
||||
(event.kind === "review" || event.kind === "review_requested") &&
|
||||
typeof event.subject_ref === "string",
|
||||
(event): event is SourceEvidenceEvent =>
|
||||
event.kind === "review" || event.kind === "review_requested",
|
||||
);
|
||||
if (latestEvidence?.subject_ref && latestEvidence.subject_ref !== source) {
|
||||
return `Fresh source review required: selector_from moved from ${latestEvidence.subject_ref} to ${source}.`;
|
||||
|
||||
@@ -82,15 +82,17 @@ export type WorkerCapabilities = SharedWorkerCapabilitySummary;
|
||||
|
||||
export type WorkerWorkdirAttachment = SharedWorkerWorkdirAttachmentSummary;
|
||||
|
||||
export type Worker = Omit<
|
||||
SharedWorkerSummary,
|
||||
"display_name" | "tags" | "worker_state" | "diagnostics"
|
||||
> & {
|
||||
display_name: string;
|
||||
tags: string[];
|
||||
worker_state?: WorkerStateSnapshot | null;
|
||||
diagnostics: Diagnostic[];
|
||||
};
|
||||
export type Worker =
|
||||
& Omit<
|
||||
SharedWorkerSummary,
|
||||
"display_name" | "tags" | "worker_state" | "diagnostics"
|
||||
>
|
||||
& {
|
||||
display_name: string;
|
||||
tags: string[];
|
||||
worker_state?: WorkerStateSnapshot | null;
|
||||
diagnostics: Diagnostic[];
|
||||
};
|
||||
|
||||
export type WorkerOperationState = "accepted" | "unsupported" | "rejected";
|
||||
|
||||
@@ -149,61 +151,22 @@ export type RepositoryDetailResponse = SharedRepositoryDetailResponse;
|
||||
export type RepositoryLogResponse = SharedRepositoryLogResponse;
|
||||
|
||||
export type {
|
||||
DerivedTicketRelation,
|
||||
ObjectiveDetail,
|
||||
ObjectiveLinkedTicketSummary,
|
||||
ObjectiveListResponse,
|
||||
ObjectiveSummary,
|
||||
TicketDetail,
|
||||
TicketEventDetail,
|
||||
TicketListResponse,
|
||||
TicketRelation,
|
||||
TicketRelationBlocker,
|
||||
TicketRelationNotice,
|
||||
TicketRelationView,
|
||||
TicketSummary,
|
||||
} from "$lib/generated/ticket-api";
|
||||
|
||||
export type ObjectiveSummary = {
|
||||
id: string;
|
||||
resource_key: string;
|
||||
title: string;
|
||||
state: string;
|
||||
updated_at?: string | null;
|
||||
summary: string;
|
||||
linked_tickets?: string[];
|
||||
record_source?: string;
|
||||
};
|
||||
|
||||
export type ObjectiveLinkedTicketSummary = {
|
||||
id: string;
|
||||
resource_key: string;
|
||||
title: string;
|
||||
state: string;
|
||||
};
|
||||
|
||||
export type ObjectiveDetail = {
|
||||
id: string;
|
||||
resource_key: string;
|
||||
title: string;
|
||||
state: string;
|
||||
created_at?: string | null;
|
||||
updated_at?: string | null;
|
||||
linked_tickets: string[];
|
||||
linked_ticket_summaries: ObjectiveLinkedTicketSummary[];
|
||||
body: string;
|
||||
body_truncated: boolean;
|
||||
record_source: string;
|
||||
};
|
||||
|
||||
export type InvalidProjectRecord = {
|
||||
label: string;
|
||||
reason: string;
|
||||
};
|
||||
|
||||
export type ObjectiveListResponse = {
|
||||
workspace_id: string;
|
||||
limit: number;
|
||||
items: ObjectiveSummary[];
|
||||
invalid_records: InvalidProjectRecord[];
|
||||
record_authority: string;
|
||||
};
|
||||
export type {
|
||||
TicketDetailDerivedRelation as DerivedTicketRelation,
|
||||
TicketDetailRelation as TicketRelation,
|
||||
TicketDetailRelationBlocker as TicketRelationBlocker,
|
||||
TicketDetailRelationNotice as TicketRelationNotice,
|
||||
TicketDetailRelationView as TicketRelationView,
|
||||
TicketListItemSummary as TicketSummary,
|
||||
} from "$lib/generated/ticket-api";
|
||||
|
||||
export type {
|
||||
CompanionCancelRequest,
|
||||
|
||||
@@ -7,7 +7,7 @@ import {
|
||||
} from "./ticket-panel.ts";
|
||||
import type {
|
||||
TicketDetail,
|
||||
TicketSummary,
|
||||
TicketListItemSummary as TicketSummary,
|
||||
} from "../../generated/ticket-api.ts";
|
||||
|
||||
declare const Deno: {
|
||||
@@ -120,7 +120,10 @@ Deno.test("ticket detail uses server-derived role assignment actions", async ()
|
||||
|
||||
assertEquals(source.includes("ticket.action_eligibility.can_queue"), true);
|
||||
assertEquals(source.includes("ticket.relations.blockers.length > 0"), true);
|
||||
assertEquals(source.includes("ticket.action_eligibility.queue_tickets"), true);
|
||||
assertEquals(
|
||||
source.includes("ticket.action_eligibility.queue_tickets"),
|
||||
true,
|
||||
);
|
||||
assertEquals(source.includes("This operation queues:"), true);
|
||||
assertEquals(source.includes("outcome.queued_tickets.join"), true);
|
||||
assertEquals(
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
import type { BrowserWorkspaceOrchestratorResponse } from "$lib/generated/worker-launch-api";
|
||||
import type { TicketDetail, TicketSummary } from "$lib/generated/ticket-api";
|
||||
import type {
|
||||
TicketDetail,
|
||||
TicketListItemSummary,
|
||||
} from "$lib/generated/ticket-api";
|
||||
|
||||
export const TICKET_STATES = [
|
||||
"planning",
|
||||
@@ -38,7 +41,7 @@ export const TICKET_LANE_PAGE_SIZE = 30;
|
||||
export type TicketLaneDefinition = (typeof LANE_DEFINITIONS)[number];
|
||||
export type TicketLaneId = TicketLaneDefinition["id"];
|
||||
export type TicketCardSummary = Pick<
|
||||
TicketSummary,
|
||||
TicketListItemSummary,
|
||||
"id" | "resource_key" | "title" | "state" | "priority" | "updated_at"
|
||||
>;
|
||||
|
||||
|
||||
@@ -1,33 +1,22 @@
|
||||
import type { PageLoad } from "./$types";
|
||||
import { loadJson } from "$lib/workspace/api/http";
|
||||
import { mergeRequestCollectionPath } from "$lib/workspace/api/merge-requests";
|
||||
import {
|
||||
mergeRequestCollectionPath,
|
||||
type MergeRequestListPage,
|
||||
} from "$lib/workspace/api/merge-requests";
|
||||
parseMergeRequestListResponse,
|
||||
TICKET_BROWSER_API_LOAD_POLICY,
|
||||
} from "$lib/workspace/api/ticket-browser";
|
||||
|
||||
export const load: PageLoad = async ({ params, fetch }) => {
|
||||
try {
|
||||
const response = await fetch(
|
||||
`${mergeRequestCollectionPath(params.workspaceId)}?limit=100`,
|
||||
);
|
||||
const body = await response.json().catch(() => ({}));
|
||||
if (!response.ok) {
|
||||
return {
|
||||
workspaceId: params.workspaceId,
|
||||
mergeRequests: null,
|
||||
error: body?.error ?? body?.message ??
|
||||
`Request failed (${response.status})`,
|
||||
};
|
||||
}
|
||||
return {
|
||||
workspaceId: params.workspaceId,
|
||||
mergeRequests: body as MergeRequestListPage,
|
||||
error: null,
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
workspaceId: params.workspaceId,
|
||||
mergeRequests: null,
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
};
|
||||
}
|
||||
const result = await loadJson(
|
||||
fetch,
|
||||
`${mergeRequestCollectionPath(params.workspaceId)}?limit=100`,
|
||||
undefined,
|
||||
parseMergeRequestListResponse,
|
||||
TICKET_BROWSER_API_LOAD_POLICY,
|
||||
);
|
||||
return {
|
||||
workspaceId: params.workspaceId,
|
||||
mergeRequests: result.data,
|
||||
error: result.error,
|
||||
};
|
||||
};
|
||||
|
||||
+11
-3
@@ -1,4 +1,5 @@
|
||||
<script lang="ts">
|
||||
import type { MergeRequestThreadEvent } from "$lib/generated/ticket-api";
|
||||
import { mergeRequestPagePath } from "$lib/workspace/api/merge-requests";
|
||||
import {
|
||||
sourceReviewFreshness,
|
||||
@@ -13,8 +14,15 @@
|
||||
return Number.isNaN(date.getTime()) ? value : date.toLocaleString();
|
||||
}
|
||||
|
||||
function textField(event: Record<string, unknown>, key: string): string | null {
|
||||
const value = event[key];
|
||||
function textField(
|
||||
event: MergeRequestThreadEvent,
|
||||
key: "subject_ref" | "decision" | "reason" | "body",
|
||||
): string | null {
|
||||
let value: unknown;
|
||||
if (key === "subject_ref" && "subject_ref" in event) value = event.subject_ref;
|
||||
if (key === "decision" && "decision" in event) value = event.decision;
|
||||
if (key === "reason" && "reason" in event) value = event.reason;
|
||||
if (key === "body" && "body" in event) value = event.body;
|
||||
return typeof value === "string" && value.length > 0 ? value : null;
|
||||
}
|
||||
|
||||
@@ -77,7 +85,7 @@
|
||||
<div>
|
||||
<header>
|
||||
<strong>{event.kind}</strong>
|
||||
<time>{prettyDate(event.at)}</time>
|
||||
<time>{prettyDate(event.created_at)}</time>
|
||||
</header>
|
||||
{#if textField(event, "subject_ref")}
|
||||
<p><code>{textField(event, "subject_ref")}</code></p>
|
||||
|
||||
@@ -1,38 +1,25 @@
|
||||
import type { PageLoad } from "./$types";
|
||||
import { loadJson } from "$lib/workspace/api/http";
|
||||
import { mergeRequestDetailPath } from "$lib/workspace/api/merge-requests";
|
||||
import {
|
||||
type MergeRequestDetail,
|
||||
mergeRequestDetailPath,
|
||||
} from "$lib/workspace/api/merge-requests";
|
||||
parseMergeRequestDetailResponse,
|
||||
TICKET_BROWSER_API_LOAD_POLICY,
|
||||
} from "$lib/workspace/api/ticket-browser";
|
||||
|
||||
export const load: PageLoad = async ({ params, fetch }) => {
|
||||
try {
|
||||
const response = await fetch(
|
||||
`${
|
||||
mergeRequestDetailPath(params.workspaceId, params.mergeRequestId)
|
||||
}?limit=100`,
|
||||
);
|
||||
const body = await response.json().catch(() => ({}));
|
||||
if (!response.ok) {
|
||||
return {
|
||||
workspaceId: params.workspaceId,
|
||||
mergeRequestId: params.mergeRequestId,
|
||||
mergeRequest: null,
|
||||
error: body?.error ?? body?.message ??
|
||||
`Request failed (${response.status})`,
|
||||
};
|
||||
}
|
||||
return {
|
||||
workspaceId: params.workspaceId,
|
||||
mergeRequestId: params.mergeRequestId,
|
||||
mergeRequest: body as MergeRequestDetail,
|
||||
error: null,
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
workspaceId: params.workspaceId,
|
||||
mergeRequestId: params.mergeRequestId,
|
||||
mergeRequest: null,
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
};
|
||||
}
|
||||
const result = await loadJson(
|
||||
fetch,
|
||||
`${
|
||||
mergeRequestDetailPath(params.workspaceId, params.mergeRequestId)
|
||||
}?limit=100`,
|
||||
undefined,
|
||||
parseMergeRequestDetailResponse,
|
||||
TICKET_BROWSER_API_LOAD_POLICY,
|
||||
);
|
||||
return {
|
||||
workspaceId: params.workspaceId,
|
||||
mergeRequestId: params.mergeRequestId,
|
||||
mergeRequest: result.data,
|
||||
error: result.error,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -1,12 +1,18 @@
|
||||
import { loadJson, workspaceApiPath } from "$lib/workspace/api/http";
|
||||
import type { ObjectiveListResponse } from "$lib/workspace/sidebar/types";
|
||||
import {
|
||||
parseObjectiveListResponse,
|
||||
TICKET_BROWSER_API_LOAD_POLICY,
|
||||
} from "$lib/workspace/api/ticket-browser";
|
||||
import type { PageLoad } from "./$types";
|
||||
|
||||
export const load: PageLoad = async ({ fetch, params }) => {
|
||||
const apiPath = (path: string) => workspaceApiPath(params.workspaceId, path);
|
||||
const objectives = await loadJson<ObjectiveListResponse>(
|
||||
const objectives = await loadJson(
|
||||
fetch,
|
||||
apiPath("/objectives"),
|
||||
undefined,
|
||||
parseObjectiveListResponse,
|
||||
TICKET_BROWSER_API_LOAD_POLICY,
|
||||
);
|
||||
|
||||
return {
|
||||
|
||||
@@ -1,18 +1,24 @@
|
||||
import { redirect } from "@sveltejs/kit";
|
||||
import { loadJson, workspaceApiPath } from "$lib/workspace/api/http";
|
||||
import {
|
||||
parseObjectiveDetail,
|
||||
TICKET_BROWSER_API_LOAD_POLICY,
|
||||
} from "$lib/workspace/api/ticket-browser";
|
||||
import {
|
||||
canonicalResourceReference,
|
||||
resourceKey,
|
||||
} from "$lib/workspace/resource-links";
|
||||
import type { ObjectiveDetail } from "$lib/workspace/sidebar/types";
|
||||
import type { PageLoad } from "./$types";
|
||||
|
||||
export const load: PageLoad = async ({ fetch, params }) => {
|
||||
const apiPath = (path: string) => workspaceApiPath(params.workspaceId, path);
|
||||
const objectiveId = resourceKey(params.objectiveId);
|
||||
const objective = await loadJson<ObjectiveDetail>(
|
||||
const objective = await loadJson(
|
||||
fetch,
|
||||
apiPath(`/objectives/${encodeURIComponent(objectiveId)}`),
|
||||
undefined,
|
||||
parseObjectiveDetail,
|
||||
TICKET_BROWSER_API_LOAD_POLICY,
|
||||
);
|
||||
|
||||
if (objective.data) {
|
||||
|
||||
@@ -3,10 +3,13 @@
|
||||
import type { ApiResult } from "$lib/workspace/api/http";
|
||||
import { loadJson, workspaceApiPath } from "$lib/workspace/api/http";
|
||||
import { parseBrowserWorkspaceOrchestratorResponse } from "$lib/workspace/api/workers";
|
||||
import {
|
||||
TICKET_BROWSER_API_LOAD_POLICY,
|
||||
parseTicketListResponse,
|
||||
} from "$lib/workspace/api/ticket-browser";
|
||||
import type {
|
||||
QueryPage,
|
||||
TicketListResponse,
|
||||
TicketSummary,
|
||||
TicketListItemSummary as TicketSummary,
|
||||
} from "$lib/generated/ticket-api";
|
||||
import { ticketHref } from "$lib/workspace/resource-links";
|
||||
import {
|
||||
@@ -70,15 +73,18 @@
|
||||
states: lane.states.join(","),
|
||||
cursor: lane.page.next_cursor,
|
||||
});
|
||||
const response = await fetch(
|
||||
const result = await loadJson(
|
||||
fetch,
|
||||
`/api/w/${encodeURIComponent(data.workspaceId)}/tickets?${search}`,
|
||||
undefined,
|
||||
parseTicketListResponse,
|
||||
TICKET_BROWSER_API_LOAD_POLICY,
|
||||
);
|
||||
if (!response.ok) {
|
||||
throw new Error(`追加読み込みに失敗しました (${response.status})`);
|
||||
if (!result.data) {
|
||||
throw new Error(result.error ?? "追加読み込みに失敗しました");
|
||||
}
|
||||
const page = (await response.json()) as TicketListResponse;
|
||||
lane.tickets = mergeTickets(lane.tickets, page.items);
|
||||
lane.page = page.page;
|
||||
lane.tickets = mergeTickets(lane.tickets, result.data.items);
|
||||
lane.page = result.data.page;
|
||||
} catch (error) {
|
||||
lane.error = error instanceof Error ? error.message : String(error);
|
||||
} finally {
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
import { parseBrowserWorkspaceOrchestratorResponse } from "$lib/workspace/api/workers";
|
||||
import { loadJson, workspaceApiPath } from "$lib/workspace/api/http";
|
||||
import {
|
||||
parseTicketListResponse,
|
||||
TICKET_BROWSER_API_LOAD_POLICY,
|
||||
} from "$lib/workspace/api/ticket-browser";
|
||||
import type { TicketListResponse } from "$lib/generated/ticket-api";
|
||||
import type { WorkspaceOrchestratorStatus } from "$lib/workspace/tickets/ticket-panel";
|
||||
import type { PageLoad } from "./$types";
|
||||
@@ -17,43 +21,51 @@ export type TicketLanePage = {
|
||||
response: TicketListResponse;
|
||||
};
|
||||
|
||||
async function loadLane(
|
||||
fetchFn: typeof fetch,
|
||||
workspaceId: string,
|
||||
laneId: TicketLaneId,
|
||||
): Promise<TicketLanePage> {
|
||||
const states = LANE_STATES[laneId];
|
||||
const search = new URLSearchParams({
|
||||
limit: "30",
|
||||
states: states.join(","),
|
||||
});
|
||||
const result = await loadJson(
|
||||
fetchFn,
|
||||
`/api/w/${encodeURIComponent(workspaceId)}/tickets?${search}`,
|
||||
undefined,
|
||||
parseTicketListResponse,
|
||||
TICKET_BROWSER_API_LOAD_POLICY,
|
||||
);
|
||||
if (!result.data) {
|
||||
throw new Error(result.error ?? `failed to load ${laneId} Ticket lane`);
|
||||
}
|
||||
return { states: [...states], response: result.data };
|
||||
}
|
||||
|
||||
export const load: PageLoad = async ({ fetch, params }) => {
|
||||
const workspaceId = params.workspaceId;
|
||||
const [entries, orchestrator] = await Promise.all([
|
||||
Promise.all(
|
||||
Object.entries(LANE_STATES).map(async ([laneId, states]) => {
|
||||
const search = new URLSearchParams({
|
||||
limit: "30",
|
||||
states: states.join(","),
|
||||
});
|
||||
const response = await fetch(
|
||||
`/api/w/${encodeURIComponent(workspaceId)}/tickets?${search}`,
|
||||
);
|
||||
if (!response.ok) {
|
||||
throw new Error(
|
||||
`failed to load ${laneId} Ticket lane (${response.status})`,
|
||||
);
|
||||
}
|
||||
return [
|
||||
laneId,
|
||||
{ states: [...states], response: await response.json() },
|
||||
] as const;
|
||||
}),
|
||||
),
|
||||
loadJson<WorkspaceOrchestratorStatus>(
|
||||
fetch,
|
||||
workspaceApiPath(workspaceId, "/orchestrator"),
|
||||
undefined,
|
||||
parseBrowserWorkspaceOrchestratorResponse,
|
||||
),
|
||||
]);
|
||||
const [readyPlanning, inprogressQueued, doneClosed, orchestrator] =
|
||||
await Promise.all([
|
||||
loadLane(fetch, workspaceId, "ready-planning"),
|
||||
loadLane(fetch, workspaceId, "inprogress-queued"),
|
||||
loadLane(fetch, workspaceId, "done-closed"),
|
||||
loadJson<WorkspaceOrchestratorStatus>(
|
||||
fetch,
|
||||
workspaceApiPath(workspaceId, "/orchestrator"),
|
||||
undefined,
|
||||
parseBrowserWorkspaceOrchestratorResponse,
|
||||
),
|
||||
]);
|
||||
|
||||
return {
|
||||
workspaceId,
|
||||
ticketLanes: Object.fromEntries(entries) as unknown as Record<
|
||||
TicketLaneId,
|
||||
TicketLanePage
|
||||
>,
|
||||
ticketLanes: {
|
||||
"ready-planning": readyPlanning,
|
||||
"inprogress-queued": inprogressQueued,
|
||||
"done-closed": doneClosed,
|
||||
},
|
||||
orchestrator,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -6,6 +6,12 @@
|
||||
workspaceApiJsonWithBody,
|
||||
workspaceApiPath,
|
||||
} from "$lib/workspace/api/http";
|
||||
import {
|
||||
TICKET_BROWSER_API_MAX_RESPONSE_BYTES,
|
||||
parseTicketDetail,
|
||||
parseTicketQueueOutcome,
|
||||
parseTicketRoleAssignmentMutationResponse,
|
||||
} from "$lib/workspace/api/ticket-browser";
|
||||
import { mergeRequestPagePath } from "$lib/workspace/api/merge-requests";
|
||||
import {
|
||||
relationLabel,
|
||||
@@ -38,11 +44,6 @@
|
||||
if (!loadedTicket) throw new Error(initialData.ticket.error ?? "ticket load failed");
|
||||
const loadedRepositories = $derived(data.repositories.data);
|
||||
|
||||
type QueueOutcome = {
|
||||
requested_ticket: string;
|
||||
queued_tickets: string[];
|
||||
};
|
||||
|
||||
let ticket = $state<TicketDetail>(loadedTicket);
|
||||
const mergeRequest = $derived(ticket.merge_request);
|
||||
let editing = $state(false);
|
||||
@@ -146,10 +147,10 @@
|
||||
busy = action;
|
||||
errorMessage = null;
|
||||
try {
|
||||
const response = await workspaceApiJsonWithBody<TicketDetail>(path, {
|
||||
const response = await workspaceApiJsonWithBody(path, {
|
||||
method,
|
||||
...(body === undefined ? {} : { body: JSON.stringify(body) }),
|
||||
});
|
||||
}, parseTicketDetail, TICKET_BROWSER_API_MAX_RESPONSE_BYTES);
|
||||
if (generation !== routeGeneration) return false;
|
||||
applyTicket(response);
|
||||
return true;
|
||||
@@ -171,12 +172,18 @@
|
||||
errorMessage = null;
|
||||
queueMessage = null;
|
||||
try {
|
||||
const outcome = await workspaceApiJsonWithBody<QueueOutcome>(
|
||||
const outcome = await workspaceApiJsonWithBody(
|
||||
`${path}/queue`,
|
||||
{ method: "POST", body: JSON.stringify({}) },
|
||||
parseTicketQueueOutcome,
|
||||
TICKET_BROWSER_API_MAX_RESPONSE_BYTES,
|
||||
);
|
||||
if (generation !== routeGeneration) return;
|
||||
const updatedTicket = await workspaceApiJson<TicketDetail>(path);
|
||||
const updatedTicket = await workspaceApiJson(
|
||||
path,
|
||||
parseTicketDetail,
|
||||
TICKET_BROWSER_API_MAX_RESPONSE_BYTES,
|
||||
);
|
||||
if (generation !== routeGeneration) return;
|
||||
queueMessage = `Queued ${outcome.queued_tickets.length} Ticket(s): ${outcome.queued_tickets.join(", ")}`;
|
||||
applyTicket(updatedTicket);
|
||||
@@ -210,9 +217,15 @@
|
||||
expected_assignment_id: null,
|
||||
}),
|
||||
},
|
||||
parseTicketRoleAssignmentMutationResponse,
|
||||
TICKET_BROWSER_API_MAX_RESPONSE_BYTES,
|
||||
);
|
||||
if (generation !== routeGeneration) return;
|
||||
const updatedTicket = await workspaceApiJson<TicketDetail>(path);
|
||||
const updatedTicket = await workspaceApiJson(
|
||||
path,
|
||||
parseTicketDetail,
|
||||
TICKET_BROWSER_API_MAX_RESPONSE_BYTES,
|
||||
);
|
||||
if (generation !== routeGeneration) return;
|
||||
applyTicket(updatedTicket);
|
||||
} catch (error) {
|
||||
@@ -317,7 +330,7 @@
|
||||
event.preventDefault();
|
||||
if (!threadBody.trim()) return;
|
||||
if (
|
||||
await mutate("thread", "/thread", {
|
||||
await mutate("thread", "/events", {
|
||||
role: threadRole,
|
||||
body: threadBody.trim(),
|
||||
})
|
||||
|
||||
@@ -1,12 +1,16 @@
|
||||
import { redirect } from "@sveltejs/kit";
|
||||
import { loadJson, workspaceApiPath } from "$lib/workspace/api/http";
|
||||
import { loadWorkspaceRepositoryList } from "$lib/workspace/api/repositories";
|
||||
import { parseBrowserWorkspaceOrchestratorResponse } from "$lib/workspace/api/workers";
|
||||
import {
|
||||
parseTicketDetail,
|
||||
TICKET_BROWSER_API_LOAD_POLICY,
|
||||
} from "$lib/workspace/api/ticket-browser";
|
||||
import {
|
||||
canonicalResourceReference,
|
||||
resourceKey,
|
||||
} from "$lib/workspace/resource-links";
|
||||
import type { WorkspaceOrchestratorStatus } from "$lib/workspace/tickets/ticket-panel";
|
||||
import type { TicketDetail } from "$lib/workspace/sidebar/types";
|
||||
import type { PageLoad } from "./$types";
|
||||
|
||||
export const load = (async ({ fetch, params }) => {
|
||||
@@ -16,11 +20,19 @@ export const load = (async ({ fetch, params }) => {
|
||||
`/tickets/${encodeURIComponent(reference)}`,
|
||||
);
|
||||
const [ticket, repositoriesRaw, orchestrator] = await Promise.all([
|
||||
loadJson<TicketDetail>(fetch, ticketPath),
|
||||
loadJson(
|
||||
fetch,
|
||||
ticketPath,
|
||||
undefined,
|
||||
parseTicketDetail,
|
||||
TICKET_BROWSER_API_LOAD_POLICY,
|
||||
),
|
||||
loadWorkspaceRepositoryList(fetch, params.workspaceId),
|
||||
loadJson<WorkspaceOrchestratorStatus>(
|
||||
fetch,
|
||||
workspaceApiPath(params.workspaceId, "/orchestrator"),
|
||||
undefined,
|
||||
parseBrowserWorkspaceOrchestratorResponse,
|
||||
),
|
||||
]);
|
||||
if (ticket.data) {
|
||||
|
||||
@@ -14,11 +14,88 @@ function assertEquals(actual: unknown, expected: unknown): void {
|
||||
}
|
||||
}
|
||||
|
||||
function event(
|
||||
kind: string,
|
||||
fields: Record<string, unknown>,
|
||||
): MergeRequestThreadEvent {
|
||||
return { kind, sequence: 1, at: "2026-09-01T00:00:00Z", ...fields };
|
||||
const worker = { runtime_id: "runtime", worker_id: "worker" };
|
||||
const createdAt = "2026-09-01T00:00:00Z";
|
||||
|
||||
type ReviewRequested = Extract<
|
||||
MergeRequestThreadEvent,
|
||||
{ kind: "review_requested" }
|
||||
>;
|
||||
type Review = Extract<MergeRequestThreadEvent, { kind: "review" }>;
|
||||
type ReviewRevoked = Extract<
|
||||
MergeRequestThreadEvent,
|
||||
{ kind: "review_revoked" }
|
||||
>;
|
||||
type ReviewCancelled = Extract<
|
||||
MergeRequestThreadEvent,
|
||||
{ kind: "review_cancelled" }
|
||||
>;
|
||||
|
||||
function reviewRequested(
|
||||
eventId: string,
|
||||
subjectRef: string,
|
||||
): ReviewRequested {
|
||||
return {
|
||||
kind: "review_requested",
|
||||
event_id: eventId,
|
||||
sequence: 1,
|
||||
subject_ref: subjectRef,
|
||||
requested_by: worker,
|
||||
reviewer: worker,
|
||||
created_at: createdAt,
|
||||
};
|
||||
}
|
||||
|
||||
function review(
|
||||
eventId: string,
|
||||
requestEventId: string,
|
||||
subjectRef: string,
|
||||
): Review {
|
||||
return {
|
||||
kind: "review",
|
||||
event_id: eventId,
|
||||
sequence: 1,
|
||||
request_event_id: requestEventId,
|
||||
subject_ref: subjectRef,
|
||||
decision: "approve",
|
||||
body: "approved",
|
||||
findings: [],
|
||||
reviewer: worker,
|
||||
created_at: createdAt,
|
||||
};
|
||||
}
|
||||
|
||||
function reviewRevoked(
|
||||
eventId: string,
|
||||
reviewEventId: string,
|
||||
subjectRef: string,
|
||||
): ReviewRevoked {
|
||||
return {
|
||||
kind: "review_revoked",
|
||||
event_id: eventId,
|
||||
sequence: 1,
|
||||
review_event_id: reviewEventId,
|
||||
subject_ref: subjectRef,
|
||||
reason: "superseded",
|
||||
revoked_by: worker,
|
||||
created_at: createdAt,
|
||||
};
|
||||
}
|
||||
|
||||
function reviewCancelled(
|
||||
eventId: string,
|
||||
requestEventId: string,
|
||||
subjectRef: string,
|
||||
): ReviewCancelled {
|
||||
return {
|
||||
kind: "review_cancelled",
|
||||
event_id: eventId,
|
||||
sequence: 1,
|
||||
request_event_id: requestEventId,
|
||||
subject_ref: subjectRef,
|
||||
reason: "cancelled",
|
||||
created_at: createdAt,
|
||||
};
|
||||
}
|
||||
|
||||
function detail(thread: MergeRequestThreadEvent[]): MergeRequestDetail {
|
||||
@@ -30,22 +107,17 @@ function detail(thread: MergeRequestThreadEvent[]): MergeRequestDetail {
|
||||
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",
|
||||
created_at: createdAt,
|
||||
updated_at: createdAt,
|
||||
source: {
|
||||
status: "known",
|
||||
ref: "source-2",
|
||||
observed_at: "2026-09-01T00:00:00Z",
|
||||
observed_at: createdAt,
|
||||
},
|
||||
target: {
|
||||
status: "known",
|
||||
ref: "target-2",
|
||||
observed_at: "2026-09-01T00:00:00Z",
|
||||
observed_at: createdAt,
|
||||
},
|
||||
linked_tickets: [{ ticket_id: "T-1", key: "T-1" }],
|
||||
thread,
|
||||
@@ -54,20 +126,9 @@ function detail(thread: MergeRequestThreadEvent[]): MergeRequestDetail {
|
||||
|
||||
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",
|
||||
}),
|
||||
reviewRequested("request-1", "source-2"),
|
||||
review("review-1", "request-1", "source-2"),
|
||||
reviewRevoked("revoke-1", "review-1", "source-2"),
|
||||
]);
|
||||
|
||||
assertEquals(
|
||||
@@ -78,10 +139,7 @@ Deno.test("revoked review requires a fresh review instead of appearing pending",
|
||||
|
||||
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",
|
||||
}),
|
||||
reviewRequested("request-2", "source-2"),
|
||||
]);
|
||||
|
||||
assertEquals(
|
||||
@@ -92,16 +150,8 @@ Deno.test("unresolved review request for the current source is pending", () => {
|
||||
|
||||
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",
|
||||
}),
|
||||
reviewRequested("request-3", "source-2"),
|
||||
review("review-3", "request-3", "source-2"),
|
||||
]);
|
||||
assertEquals(
|
||||
sourceReviewFreshness(approved),
|
||||
@@ -109,15 +159,8 @@ Deno.test("completed or cancelled request is not projected as pending", () => {
|
||||
);
|
||||
|
||||
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",
|
||||
}),
|
||||
reviewRequested("request-4", "source-2"),
|
||||
reviewCancelled("cancel-4", "request-4", "source-2"),
|
||||
]);
|
||||
assertEquals(
|
||||
sourceReviewFreshness(cancelled),
|
||||
@@ -127,12 +170,7 @@ Deno.test("completed or cancelled request is not projected as pending", () => {
|
||||
|
||||
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",
|
||||
}),
|
||||
review("review-old", "request-old", "source-1"),
|
||||
]);
|
||||
|
||||
assertEquals(
|
||||
|
||||
Reference in New Issue
Block a user