fix: enforce ServerApi as the wire authority

This commit is contained in:
2026-09-23 17:27:27 +09:00
parent f25c8d8b6d
commit 2766e5b41b
15 changed files with 2880 additions and 6444 deletions
+17 -17
View File
@@ -19,10 +19,10 @@ use crate::records::{
ObjectiveQueryItem, ObjectiveQueryRequest, ObjectiveQueryResponse, ObjectiveResourceSummary,
ObjectiveShowRequest, ObjectiveSummary, ProjectRecordList, QueryPage, TicketActionEligibility,
TicketAssignmentPrincipalSummary, TicketAssignmentSummary, TicketDetail, TicketEventDetail,
TicketEvidenceEvent, TicketEvidenceSummary, TicketListPageRequest, TicketMergeRequestSummary,
TicketQueryItem, TicketQueryRequest, TicketQueryResponse, TicketRelationView,
TicketEvidenceEvent, TicketEvidenceSummary, TicketListProjectionRequest,
TicketMergeRequestSummary, TicketQueryItem, TicketQueryRequest, TicketQueryResponse,
TicketRoleAssignmentSummary, TicketShowRequest, TicketSummary, TicketSummaryPage,
summarize_body, truncate_body,
summarize_body, ticket_relation_view_from_domain, truncate_body,
};
use crate::store::{
ControlPlaneStore, MemoryDocumentRecord, MemoryStagingRecord, MemoryStagingResolutionRecord,
@@ -48,7 +48,7 @@ impl<T> WorkspaceAuthority for T where T: ObjectiveAuthority + TicketAuthority +
pub trait TicketAuthority {
fn list_tickets(&self, limit: usize) -> Result<ProjectRecordList<TicketSummary>>;
fn list_ticket_page(&self, request: TicketListPageRequest) -> Result<TicketSummaryPage>;
fn list_ticket_page(&self, request: TicketListProjectionRequest) -> Result<TicketSummaryPage>;
fn query_tickets(&self, query: TicketQueryRequest) -> Result<TicketQueryResponse>;
fn ticket(&self, id: &str) -> Result<TicketDetail>;
fn show_ticket(&self, id: &str, query: TicketShowRequest) -> Result<TicketDetail>;
@@ -86,14 +86,14 @@ pub trait MemoryAuthority {
fn ensure_memory_document(&self) -> Result<MemoryDocument>;
fn memory_document(&self) -> Result<MemoryDocument>;
fn update_memory_document(&self, body_md: &str) -> Result<MemoryDocument>;
fn list_memory_staging_records(&self, limit: usize) -> Result<Vec<MemoryStagingEntry>>;
fn memory_staging_record(&self, candidate_id: &str) -> Result<MemoryStagingEntry>;
fn list_memory_staging_records(&self, limit: usize) -> Result<Vec<InternalMemoryStagingEntry>>;
fn memory_staging_record(&self, candidate_id: &str) -> Result<InternalMemoryStagingEntry>;
fn upsert_memory_staging_record(
&self,
candidate_id: &str,
raw_json: &str,
source_path: Option<&str>,
) -> Result<MemoryStagingEntry>;
) -> Result<InternalMemoryStagingEntry>;
fn close_memory_staging_record(
&self,
candidate_id: &str,
@@ -114,7 +114,7 @@ pub struct MemoryDocument {
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct MemoryStagingEntry {
pub struct InternalMemoryStagingEntry {
pub candidate_id: String,
pub raw_json: String,
pub source_path: Option<String>,
@@ -966,7 +966,7 @@ impl SqliteWorkspaceAuthority {
&ticket.meta.id,
)?)
.ok_or_else(|| Error::Store(format!("missing resource key for {}", ticket.meta.id)))?;
let mut relations: TicketRelationView = ticket.relations.into();
let mut relations = ticket_relation_view_from_domain(ticket.relations);
for relation in &mut relations.outgoing {
relation.target_resource_key = self.store.resource_key(
&self.workspace_id,
@@ -1073,7 +1073,7 @@ impl TicketAuthority for SqliteWorkspaceAuthority {
})
}
fn list_ticket_page(&self, request: TicketListPageRequest) -> Result<TicketSummaryPage> {
fn list_ticket_page(&self, request: TicketListProjectionRequest) -> Result<TicketSummaryPage> {
let limit = request.limit.unwrap_or(30).clamp(1, 100);
let mut states = request.states;
states.sort();
@@ -1587,7 +1587,7 @@ impl MemoryAuthority for SqliteWorkspaceAuthority {
Ok(memory_document_from_record(record))
}
fn list_memory_staging_records(&self, limit: usize) -> Result<Vec<MemoryStagingEntry>> {
fn list_memory_staging_records(&self, limit: usize) -> Result<Vec<InternalMemoryStagingEntry>> {
self.store
.list_memory_staging_records(&self.workspace_id, limit)
.map(|records| {
@@ -1598,7 +1598,7 @@ impl MemoryAuthority for SqliteWorkspaceAuthority {
})
}
fn memory_staging_record(&self, candidate_id: &str) -> Result<MemoryStagingEntry> {
fn memory_staging_record(&self, candidate_id: &str) -> Result<InternalMemoryStagingEntry> {
validate_memory_candidate_id(candidate_id)?;
self.store
.get_memory_staging_record(&self.workspace_id, candidate_id)?
@@ -1613,7 +1613,7 @@ impl MemoryAuthority for SqliteWorkspaceAuthority {
candidate_id: &str,
raw_json: &str,
source_path: Option<&str>,
) -> Result<MemoryStagingEntry> {
) -> Result<InternalMemoryStagingEntry> {
validate_memory_candidate_id(candidate_id)?;
validate_json_object(raw_json, "raw_json")?;
let imported_at = now_rfc3339();
@@ -2634,8 +2634,8 @@ fn memory_document_from_record(record: MemoryDocumentRecord) -> MemoryDocument {
}
}
fn memory_staging_from_record(record: MemoryStagingRecord) -> MemoryStagingEntry {
MemoryStagingEntry {
fn memory_staging_from_record(record: MemoryStagingRecord) -> InternalMemoryStagingEntry {
InternalMemoryStagingEntry {
candidate_id: record.candidate_id,
raw_json: record.raw_json,
source_path: record.source_path,
@@ -3330,7 +3330,7 @@ VALUES ('workspace-test', 'ticket', 4);
assert_eq!(incoming_relation.items.len(), 1);
assert_eq!(incoming_relation.items[0].id, "00000000001J5");
let summary_page = authority
.list_ticket_page(TicketListPageRequest {
.list_ticket_page(TicketListProjectionRequest {
states: vec!["planning".to_string(), "ready".to_string()],
limit: Some(1),
cursor: None,
@@ -3338,7 +3338,7 @@ VALUES ('workspace-test', 'ticket', 4);
.unwrap();
assert_eq!(summary_page.items.len(), 1);
assert!(summary_page.page.has_more);
let mismatched_summary_cursor = authority.list_ticket_page(TicketListPageRequest {
let mismatched_summary_cursor = authority.list_ticket_page(TicketListProjectionRequest {
states: vec!["done".to_string()],
limit: Some(1),
cursor: summary_page.page.next_cursor,
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -36,7 +36,7 @@ pub mod workspace_signing_identity;
mod workspace_subscription;
pub use authority::{
MemoryAuthority, MemoryDocument, MemoryStagingEntry, MemoryStagingResolution,
InternalMemoryStagingEntry, MemoryAuthority, MemoryDocument, MemoryStagingResolution,
ObjectiveAuthority, SqliteWorkspaceAuthority, TicketAuthority, TicketMergeRevisionSource,
WorkspaceAuthority,
};
+68 -409
View File
@@ -1,19 +1,29 @@
use serde::{Deserialize, Serialize};
pub use server_api::{
ObjectiveDetail, ObjectiveEventDetail, ObjectiveLinkedTicketSummary, ObjectiveResourceSummary,
ObjectiveSummary, QueryPage,
InvalidProjectRecord, MergeRequestListItem, MergeRequestListResponse,
MergeRequestRefDiagnostic, ObjectiveDetail, ObjectiveEventDetail, ObjectiveLinkSummary,
ObjectiveLinkedTicketSummary, ObjectiveQueryItem, ObjectiveQueryRequest,
ObjectiveQueryResponse, ObjectiveResourceSummary, ObjectiveShowRequest, ObjectiveSummary,
QueryPage, TicketActionEligibility,
TicketAssignmentPrincipal as TicketAssignmentPrincipalSummary, TicketAssignmentSummary,
TicketDetail, TicketDetailDerivedRelation as DerivedTicketRelation,
TicketDetailRelation as TicketRelation, TicketDetailRelationBlocker as TicketRelationBlocker,
TicketDetailRelationNotice as TicketRelationNotice,
TicketDetailRelationView as TicketRelationView, TicketEventDetail, TicketEvidenceEvent,
TicketEvidenceSummary, TicketListItemSummary as TicketSummary, TicketListResponse,
TicketMergeRequestSummary, TicketQueryItem, TicketQueryRequest, TicketQueryResponse,
TicketRoleAssignmentSummary, TicketShowRequest,
};
const SUMMARY_BODY_LIMIT: usize = 240;
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ProjectRecordList<T> {
pub items: Vec<T>,
pub invalid_records: Vec<InvalidProjectRecord>,
pub record_authority: String,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct TicketSummaryPage {
pub items: Vec<TicketSummary>,
pub page: QueryPage,
@@ -21,418 +31,67 @@ pub struct TicketSummaryPage {
pub record_authority: String,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct InvalidProjectRecord {
pub label: String,
pub reason: String,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct TicketSummary {
pub id: String,
pub resource_key: String,
pub title: String,
pub state: String,
pub priority: String,
pub updated_at: Option<String>,
pub queued_by: Option<String>,
pub queued_at: Option<String>,
pub workspace_action_priority: String,
pub record_source: String,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Default)]
pub struct TicketListPageRequest {
#[serde(default)]
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub struct TicketListProjectionRequest {
pub states: Vec<String>,
#[serde(default)]
pub limit: Option<usize>,
#[serde(default)]
pub cursor: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct TicketListResponse {
pub workspace_id: String,
pub limit: usize,
pub items: Vec<TicketSummary>,
pub page: QueryPage,
pub invalid_records: Vec<InvalidProjectRecord>,
pub record_authority: String,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct TicketDetail {
pub id: String,
pub resource_key: String,
pub title: String,
pub state: String,
pub readiness: Option<String>,
pub priority: String,
pub created_at: Option<String>,
pub updated_at: Option<String>,
pub item_revision: String,
pub queued_by: Option<String>,
pub queued_at: Option<String>,
pub repository_key: Option<String>,
pub ref_selector: Option<String>,
pub risk_flags: Vec<String>,
pub body: String,
pub body_truncated: bool,
pub event_count: usize,
pub events: Vec<TicketEventDetail>,
pub event_page: QueryPage,
pub artifact_count: usize,
pub artifacts: Vec<String>,
pub relations: TicketRelationView,
pub linked_objectives: Vec<ObjectiveLinkSummary>,
pub implementation_reports: Vec<TicketEvidenceEvent>,
pub assignments: Vec<TicketRoleAssignmentSummary>,
pub current_coder: Option<TicketAssignmentSummary>,
pub assignment_diagnostics: Vec<String>,
pub action_eligibility: TicketActionEligibility,
pub merge_request: Option<TicketMergeRequestSummary>,
pub evidence: TicketEvidenceSummary,
pub resolution: Option<String>,
pub record_source: String,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct TicketEventDetail {
pub sequence: usize,
pub event_ref: String,
pub kind: String,
pub author: Option<String>,
pub at: Option<String>,
pub status: Option<String>,
pub from: Option<String>,
pub to: Option<String>,
pub reason: Option<String>,
pub state_field: Option<String>,
pub heading: Option<String>,
pub body: Option<String>,
pub attributes: std::collections::BTreeMap<String, String>,
pub references: Vec<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct TicketRelation {
pub ticket_id: String,
pub kind: String,
pub target: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub target_resource_key: Option<String>,
pub note: Option<String>,
pub author: String,
pub at: String,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct DerivedTicketRelation {
pub source_ticket: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub source_resource_key: Option<String>,
pub inverse_kind: String,
pub forward_kind: String,
pub note: Option<String>,
pub author: String,
pub at: String,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct TicketRelationBlocker {
pub blocking_ticket: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub blocking_resource_key: Option<String>,
pub reason_kind: String,
pub relation_kind: String,
pub note: Option<String>,
pub blocking_state: String,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct TicketRelationNotice {
pub related_ticket: String,
pub kind: String,
pub message: String,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct TicketRelationView {
pub outgoing: Vec<TicketRelation>,
pub incoming: Vec<DerivedTicketRelation>,
pub blockers: Vec<TicketRelationBlocker>,
pub notices: Vec<TicketRelationNotice>,
}
impl From<ticket::TicketRelationView> for TicketRelationView {
fn from(value: ticket::TicketRelationView) -> Self {
Self {
outgoing: value
.outgoing
.into_iter()
.map(|relation| TicketRelation {
ticket_id: relation.ticket_id,
kind: relation.kind.as_str().to_string(),
target: relation.target,
target_resource_key: None,
note: relation.note,
author: relation.author,
at: relation.at,
})
.collect(),
incoming: value
.incoming
.into_iter()
.map(|relation| DerivedTicketRelation {
source_ticket: relation.source_ticket,
source_resource_key: None,
inverse_kind: relation.inverse_kind,
forward_kind: relation.forward_kind.as_str().to_string(),
note: relation.note,
author: relation.author,
at: relation.at,
})
.collect(),
blockers: value
.blockers
.into_iter()
.map(|blocker| TicketRelationBlocker {
blocking_ticket: blocker.blocking_ticket,
blocking_resource_key: None,
reason_kind: blocker.reason_kind,
relation_kind: blocker.relation_kind.as_str().to_string(),
note: blocker.note,
blocking_state: blocker.blocking_state.as_str().to_string(),
})
.collect(),
notices: value
.notices
.into_iter()
.map(|notice| TicketRelationNotice {
related_ticket: notice.related_ticket,
kind: notice.kind.as_str().to_string(),
message: notice.message,
})
.collect(),
}
pub(crate) fn ticket_relation_view_from_domain(
value: ticket::TicketRelationView,
) -> TicketRelationView {
TicketRelationView {
outgoing: value
.outgoing
.into_iter()
.map(|relation| TicketRelation {
ticket_id: relation.ticket_id,
kind: relation.kind.as_str().to_string(),
target: relation.target,
target_resource_key: None,
note: relation.note,
author: relation.author,
at: relation.at,
})
.collect(),
incoming: value
.incoming
.into_iter()
.map(|relation| DerivedTicketRelation {
source_ticket: relation.source_ticket,
source_resource_key: None,
inverse_kind: relation.inverse_kind,
forward_kind: relation.forward_kind.as_str().to_string(),
note: relation.note,
author: relation.author,
at: relation.at,
})
.collect(),
blockers: value
.blockers
.into_iter()
.map(|blocker| TicketRelationBlocker {
blocking_ticket: blocker.blocking_ticket,
blocking_resource_key: None,
reason_kind: blocker.reason_kind,
relation_kind: blocker.relation_kind.as_str().to_string(),
note: blocker.note,
blocking_state: blocker.blocking_state.as_str().to_string(),
})
.collect(),
notices: value
.notices
.into_iter()
.map(|notice| TicketRelationNotice {
related_ticket: notice.related_ticket,
kind: notice.kind.as_str().to_string(),
message: notice.message,
})
.collect(),
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct ObjectiveLinkSummary {
pub id: String,
pub resource_key: String,
pub title: String,
pub state: String,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct TicketEvidenceEvent {
pub event_ref: String,
pub sequence: usize,
pub kind: String,
pub at: Option<String>,
pub author: Option<String>,
pub excerpt: String,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct TicketAssignmentSummary {
pub assignment_id: String,
pub runtime_id: String,
pub worker_id: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub worker_resource_key: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct TicketRoleAssignmentSummary {
pub assignment_id: String,
pub role: String,
pub principal: TicketAssignmentPrincipalSummary,
pub assigned_by: String,
pub assigned_at: String,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(tag = "kind", rename_all = "snake_case")]
pub enum TicketAssignmentPrincipalSummary {
User {
account_id: String,
},
Worker {
runtime_id: String,
worker_id: String,
},
WorkspaceAgent {
agent_key: String,
},
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct TicketActionEligibility {
pub can_assign_orchestrator: bool,
pub can_unassign_orchestrator: bool,
pub can_queue: bool,
pub can_start_manual_coder: bool,
pub queue_tickets: Vec<String>,
pub blockers: Vec<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct TicketMergeRequestSummary {
pub merge_request_id: String,
pub repository_key: String,
pub state: String,
pub review_status: String,
pub selector_from: Option<String>,
pub selector_to: String,
pub updated_at: String,
pub current_subject_ref: Option<String>,
pub review_subject_ref: Option<String>,
pub review_requested_at: Option<String>,
pub review_submitted_at: Option<String>,
pub review_excerpt: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct MergeRequestRefDiagnostic {
pub code: String,
pub message: String,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct MergeRequestListItem {
pub summary: TicketMergeRequestSummary,
pub ticket_ids: Vec<String>,
pub thread_event_count: usize,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub ref_diagnostics: Vec<MergeRequestRefDiagnostic>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct MergeRequestListResponse {
pub items: Vec<MergeRequestListItem>,
pub next_cursor: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Default)]
pub struct TicketEvidenceSummary {
pub has_merge_request: bool,
pub has_current_subject_ref: bool,
pub has_review_request: bool,
pub has_commit: bool,
pub review_status: Option<String>,
pub approved_current_subject: bool,
pub review_after_rescope: bool,
pub unresolved_request_changes: bool,
pub complete_for_integration: bool,
pub missing: Vec<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Default)]
pub struct TicketQueryRequest {
pub query: Option<String>,
#[serde(default)]
pub states: Vec<String>,
#[serde(default)]
pub event_kinds: Vec<String>,
#[serde(default)]
pub evidence: Vec<String>,
pub review_status: Option<String>,
#[serde(default)]
pub attention: Vec<String>,
pub related_ticket_id: Option<String>,
pub relation_kind: Option<String>,
pub linked_objective_id: Option<String>,
pub updated_after: Option<String>,
pub updated_before: Option<String>,
pub sort: Option<String>,
pub limit: Option<usize>,
pub cursor: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct TicketQueryItem {
pub id: String,
pub resource_key: String,
pub title: String,
pub state: String,
pub readiness: Option<String>,
pub priority: String,
pub created_at: Option<String>,
pub updated_at: Option<String>,
pub item_revision: String,
pub workspace_action_priority: String,
pub matched_fields: Vec<String>,
pub snippet: Option<String>,
pub matching_event: Option<TicketEvidenceEvent>,
pub linked_objective_ids: Vec<String>,
pub linked_objective_keys: Vec<String>,
pub relation_count: usize,
pub blocker_count: usize,
pub unresolved_blocker_count: usize,
pub unresolved_review_count: usize,
pub evidence: TicketEvidenceSummary,
pub merge_request: Option<TicketMergeRequestSummary>,
pub current_coder: Option<TicketAssignmentSummary>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct TicketQueryResponse {
pub items: Vec<TicketQueryItem>,
pub page: QueryPage,
pub record_authority: String,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Default)]
pub struct TicketShowRequest {
pub event_limit: Option<usize>,
pub event_cursor: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Default)]
pub struct ObjectiveQueryRequest {
pub query: Option<String>,
#[serde(default)]
pub states: Vec<String>,
pub linked_ticket_id: Option<String>,
pub updated_after: Option<String>,
pub updated_before: Option<String>,
pub sort: Option<String>,
pub limit: Option<usize>,
pub cursor: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct ObjectiveQueryItem {
pub id: String,
pub resource_key: String,
pub title: String,
pub state: String,
pub created_at: Option<String>,
pub updated_at: Option<String>,
pub matched_fields: Vec<String>,
pub snippet: Option<String>,
pub linked_ticket_count: usize,
pub linked_tickets: Vec<String>,
pub linked_ticket_keys: Vec<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct ObjectiveQueryResponse {
pub items: Vec<ObjectiveQueryItem>,
pub page: QueryPage,
pub record_authority: String,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Default)]
pub struct ObjectiveShowRequest {
pub event_limit: Option<usize>,
pub event_cursor: Option<String>,
}
pub(crate) fn summarize_body(body: &str) -> String {
let summary = body
.lines()
File diff suppressed because it is too large Load Diff