fix: align read projections with query contracts
This commit is contained in:
@@ -422,12 +422,12 @@ fn list_schema() -> serde_json::Value {
|
|||||||
"type":"object",
|
"type":"object",
|
||||||
"additionalProperties": false,
|
"additionalProperties": false,
|
||||||
"properties":{
|
"properties":{
|
||||||
"text":{"type":["string","null"]},
|
"query":{"type":["string","null"]},
|
||||||
"states":{"type":"array","items":{"type":"string"},"default":[]},
|
"states":{"type":"array","items":{"type":"string"},"default":[]},
|
||||||
"linked_ticket_id":{"type":["string","null"]},
|
"linked_ticket_id":{"type":["string","null"]},
|
||||||
"updated_after":{"type":["string","null"]},
|
"updated_after":{"type":["string","null"]},
|
||||||
"updated_before":{"type":["string","null"]},
|
"updated_before":{"type":["string","null"]},
|
||||||
"sort":{"type":["string","null"],"enum":["updated_desc","title",null]},
|
"sort":{"type":["string","null"],"enum":["relevance","updated_desc","created_desc","title",null]},
|
||||||
"limit":{"type":["integer","null"],"minimum":1,"maximum":100},
|
"limit":{"type":["integer","null"],"minimum":1,"maximum":100},
|
||||||
"cursor":{"type":["string","null"]}
|
"cursor":{"type":["string","null"]}
|
||||||
}
|
}
|
||||||
@@ -510,7 +510,7 @@ fn id_ticket_schema(required: &[&str]) -> serde_json::Value {
|
|||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize)]
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
struct QueryObjectiveInput {
|
struct QueryObjectiveInput {
|
||||||
text: Option<String>,
|
query: Option<String>,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
states: Vec<String>,
|
states: Vec<String>,
|
||||||
linked_ticket_id: Option<String>,
|
linked_ticket_id: Option<String>,
|
||||||
|
|||||||
@@ -69,33 +69,103 @@ impl WorkspaceTicketReadKind {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize, Serialize, JsonSchema)]
|
||||||
|
#[serde(rename_all = "snake_case")]
|
||||||
|
enum WorkspaceTicketStateFilter {
|
||||||
|
Planning,
|
||||||
|
Ready,
|
||||||
|
Queued,
|
||||||
|
Inprogress,
|
||||||
|
Done,
|
||||||
|
Closed,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize, Serialize, JsonSchema)]
|
||||||
|
#[serde(rename_all = "snake_case")]
|
||||||
|
enum WorkspaceTicketEvidenceFilter {
|
||||||
|
ImplementationReport,
|
||||||
|
ImplementationReportAfterRescope,
|
||||||
|
MergeRequest,
|
||||||
|
Commit,
|
||||||
|
ApprovedReview,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize, Serialize, JsonSchema)]
|
||||||
|
#[serde(rename_all = "snake_case")]
|
||||||
|
enum WorkspaceTicketReviewFilter {
|
||||||
|
None,
|
||||||
|
Pending,
|
||||||
|
Approved,
|
||||||
|
RequestChanges,
|
||||||
|
UnresolvedChanges,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize, Serialize, JsonSchema)]
|
||||||
|
#[serde(rename_all = "snake_case")]
|
||||||
|
enum WorkspaceTicketAttentionFilter {
|
||||||
|
DoneNotClosed,
|
||||||
|
ImplementationReportNotClosed,
|
||||||
|
ReportAfterRescope,
|
||||||
|
UnresolvedReview,
|
||||||
|
MissingCommit,
|
||||||
|
Blocked,
|
||||||
|
Unblocked,
|
||||||
|
Ready,
|
||||||
|
AwaitingReview,
|
||||||
|
UnresolvedChanges,
|
||||||
|
StaleAfterRescope,
|
||||||
|
MissingEvidence,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize, Serialize, JsonSchema)]
|
||||||
|
#[serde(rename_all = "snake_case")]
|
||||||
|
enum WorkspaceTicketRelationFilter {
|
||||||
|
DependsOn,
|
||||||
|
Blocks,
|
||||||
|
Related,
|
||||||
|
Supersedes,
|
||||||
|
DuplicateOf,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize, Serialize, JsonSchema)]
|
||||||
|
#[serde(rename_all = "snake_case")]
|
||||||
|
enum WorkspaceTicketSort {
|
||||||
|
Relevance,
|
||||||
|
UpdatedDesc,
|
||||||
|
CreatedDesc,
|
||||||
|
Priority,
|
||||||
|
Title,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Deserialize, Serialize, JsonSchema)]
|
#[derive(Debug, Deserialize, Serialize, JsonSchema)]
|
||||||
struct WorkspaceQueryTicketInput {
|
struct WorkspaceQueryTicketInput {
|
||||||
/// Full-text match over Ticket title, item body, and bounded thread excerpts.
|
/// Full-text match over Ticket title, item body, and bounded thread excerpts.
|
||||||
text: Option<String>,
|
query: Option<String>,
|
||||||
/// Exact workflow states. Empty means every state.
|
/// Exact workflow states. Empty means every state.
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
states: Vec<String>,
|
states: Vec<WorkspaceTicketStateFilter>,
|
||||||
/// Exact typed event kinds that must occur in the bounded thread window.
|
/// Exact typed event kinds that must occur in the bounded thread window.
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
event_kinds: Vec<String>,
|
event_kinds: Vec<String>,
|
||||||
/// Required evidence kinds: implementation_report, implementation_report_after_rescope,
|
/// Required evidence kinds: implementation_report, implementation_report_after_rescope,
|
||||||
/// merge_request, commit, or approved_review.
|
/// merge_request, commit, or approved_review.
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
evidence: Vec<String>,
|
evidence: Vec<WorkspaceTicketEvidenceFilter>,
|
||||||
/// Current authoritative Merge Request review status.
|
/// Current authoritative Merge Request review status: none, pending, approved,
|
||||||
review_status: Option<String>,
|
/// request_changes, or unresolved_changes.
|
||||||
/// Attention filters: blocked, ready, awaiting_review, unresolved_changes,
|
review_status: Option<WorkspaceTicketReviewFilter>,
|
||||||
/// stale_after_rescope, or missing_evidence.
|
/// Attention filters include done_not_closed, implementation_report_not_closed,
|
||||||
|
/// report_after_rescope, unresolved_review, missing_commit, blocked, and unblocked.
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
attention: Vec<String>,
|
attention: Vec<WorkspaceTicketAttentionFilter>,
|
||||||
related_ticket_id: Option<String>,
|
related_ticket_id: Option<String>,
|
||||||
relation_kind: Option<String>,
|
relation_kind: Option<WorkspaceTicketRelationFilter>,
|
||||||
linked_objective_id: Option<String>,
|
linked_objective_id: Option<String>,
|
||||||
updated_after: Option<String>,
|
updated_after: Option<String>,
|
||||||
updated_before: Option<String>,
|
updated_before: Option<String>,
|
||||||
/// updated_desc (default), priority, or title.
|
/// relevance (default when query is present), updated_desc, created_desc,
|
||||||
sort: Option<String>,
|
/// priority, or title.
|
||||||
|
sort: Option<WorkspaceTicketSort>,
|
||||||
/// Page size; bounded by the Backend to 1..=100.
|
/// Page size; bounded by the Backend to 1..=100.
|
||||||
limit: Option<usize>,
|
limit: Option<usize>,
|
||||||
/// Opaque cursor returned by a prior QueryTicket page.
|
/// Opaque cursor returned by a prior QueryTicket page.
|
||||||
@@ -306,14 +376,7 @@ impl TicketFeatureAccess {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const READ_ONLY_TOOL_NAMES: &[&str] = &[
|
const READ_ONLY_TOOL_NAMES: &[&str] = &["QueryTicket", "ShowTicket"];
|
||||||
"QueryTicket",
|
|
||||||
"ShowTicket",
|
|
||||||
"TicketDependencyCheck",
|
|
||||||
"TicketDoctor",
|
|
||||||
"TicketRelationQuery",
|
|
||||||
"TicketOrchestrationPlanQuery",
|
|
||||||
];
|
|
||||||
|
|
||||||
const AUTHORING_TOOL_NAMES: &[&str] = &[
|
const AUTHORING_TOOL_NAMES: &[&str] = &[
|
||||||
"TicketCreate",
|
"TicketCreate",
|
||||||
@@ -337,12 +400,8 @@ const WORKSPACE_AUTHORING_TOOL_NAMES: &[&str] = &[
|
|||||||
"TicketComment",
|
"TicketComment",
|
||||||
"TicketQueue",
|
"TicketQueue",
|
||||||
"TicketClose",
|
"TicketClose",
|
||||||
"TicketDependencyCheck",
|
|
||||||
"TicketDoctor",
|
|
||||||
"TicketRelationRecord",
|
"TicketRelationRecord",
|
||||||
"TicketRelationRemove",
|
"TicketRelationRemove",
|
||||||
"TicketRelationQuery",
|
|
||||||
"TicketOrchestrationPlanQuery",
|
|
||||||
];
|
];
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
@@ -353,10 +412,8 @@ const WORKFLOW_TOOL_NAMES: &[&str] = &[
|
|||||||
"TicketWorkflowState",
|
"TicketWorkflowState",
|
||||||
"TicketClose",
|
"TicketClose",
|
||||||
"TicketDependencyCheck",
|
"TicketDependencyCheck",
|
||||||
"TicketDoctor",
|
|
||||||
"TicketRelationRecord",
|
"TicketRelationRecord",
|
||||||
"TicketRelationRemove",
|
"TicketRelationRemove",
|
||||||
"TicketRelationQuery",
|
|
||||||
"TicketOrchestrationPlanRecord",
|
"TicketOrchestrationPlanRecord",
|
||||||
"TicketOrchestrationPlanQuery",
|
"TicketOrchestrationPlanQuery",
|
||||||
];
|
];
|
||||||
@@ -364,9 +421,11 @@ const WORKFLOW_TOOL_NAMES: &[&str] = &[
|
|||||||
const WORKFLOW_ADDITIONAL_TOOL_NAMES: &[&str] = &[
|
const WORKFLOW_ADDITIONAL_TOOL_NAMES: &[&str] = &[
|
||||||
"TicketWorkflowState",
|
"TicketWorkflowState",
|
||||||
"TicketClose",
|
"TicketClose",
|
||||||
|
"TicketDependencyCheck",
|
||||||
"TicketRelationRecord",
|
"TicketRelationRecord",
|
||||||
"TicketRelationRemove",
|
"TicketRelationRemove",
|
||||||
"TicketOrchestrationPlanRecord",
|
"TicketOrchestrationPlanRecord",
|
||||||
|
"TicketOrchestrationPlanQuery",
|
||||||
];
|
];
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
@@ -1232,10 +1291,30 @@ mod tests {
|
|||||||
assert!(query.input_schema["properties"]["evidence"].is_object());
|
assert!(query.input_schema["properties"]["evidence"].is_object());
|
||||||
assert!(query.input_schema["properties"]["attention"].is_object());
|
assert!(query.input_schema["properties"]["attention"].is_object());
|
||||||
assert!(query.input_schema["properties"]["cursor"].is_object());
|
assert!(query.input_schema["properties"]["cursor"].is_object());
|
||||||
|
let query_schema = serde_json::to_string(&query.input_schema).unwrap();
|
||||||
|
assert!(query_schema.contains("done_not_closed"));
|
||||||
|
assert!(query_schema.contains("request_changes"));
|
||||||
|
assert!(query_schema.contains("created_desc"));
|
||||||
|
assert!(
|
||||||
|
query_schema.len() < 8_000,
|
||||||
|
"QueryTicket schema grew unexpectedly"
|
||||||
|
);
|
||||||
let (show, _) = workspace_ticket_read_definition(client, WorkspaceTicketReadKind::Show)();
|
let (show, _) = workspace_ticket_read_definition(client, WorkspaceTicketReadKind::Show)();
|
||||||
assert_eq!(show.name, "ShowTicket");
|
assert_eq!(show.name, "ShowTicket");
|
||||||
assert!(show.input_schema["properties"]["event_limit"].is_object());
|
assert!(show.input_schema["properties"]["event_limit"].is_object());
|
||||||
let tool_names = TicketFeatureAccess::workspace_authoring().tool_names();
|
let tool_names = TicketFeatureAccess::workspace_authoring().tool_names();
|
||||||
|
assert_eq!(tool_names.len(), 9);
|
||||||
|
assert!(
|
||||||
|
tool_names.len() < 13,
|
||||||
|
"authoring catalog must stay below the prior broad catalog"
|
||||||
|
);
|
||||||
|
let workflow_names = TicketFeatureAccess::workflow().tool_names();
|
||||||
|
assert_eq!(workflow_names.len(), 10);
|
||||||
|
assert!(
|
||||||
|
workflow_names.len() < 12,
|
||||||
|
"workflow catalog must stay below the prior broad catalog"
|
||||||
|
);
|
||||||
|
assert_eq!(TicketFeatureAccess::review().tool_names().len(), 2);
|
||||||
assert!(tool_names.contains(&"QueryTicket"));
|
assert!(tool_names.contains(&"QueryTicket"));
|
||||||
assert!(tool_names.contains(&"ShowTicket"));
|
assert!(tool_names.contains(&"ShowTicket"));
|
||||||
assert!(!tool_names.contains(&"TicketList"));
|
assert!(!tool_names.contains(&"TicketList"));
|
||||||
|
|||||||
@@ -10,12 +10,12 @@ use ticket::{
|
|||||||
};
|
};
|
||||||
|
|
||||||
use crate::records::{
|
use crate::records::{
|
||||||
ObjectiveDetail, ObjectiveEventDetail, ObjectiveLinkSummary, ObjectiveQueryItem,
|
ObjectiveDetail, ObjectiveEventDetail, ObjectiveLinkSummary, ObjectiveLinkedTicketSummary,
|
||||||
ObjectiveQueryRequest, ObjectiveQueryResponse, ObjectiveResourceSummary, ObjectiveShowRequest,
|
ObjectiveQueryItem, ObjectiveQueryRequest, ObjectiveQueryResponse, ObjectiveResourceSummary,
|
||||||
ObjectiveSummary, ProjectRecordList, QueryPage, TicketDetail, TicketEventDetail,
|
ObjectiveShowRequest, ObjectiveSummary, ProjectRecordList, QueryPage, TicketAssignmentSummary,
|
||||||
TicketEvidenceEvent, TicketEvidenceSummary, TicketMergeRequestSummary, TicketQueryItem,
|
TicketDetail, TicketEventDetail, TicketEvidenceEvent, TicketEvidenceSummary,
|
||||||
TicketQueryRequest, TicketQueryResponse, TicketShowRequest, TicketSummary, summarize_body,
|
TicketMergeRequestSummary, TicketQueryItem, TicketQueryRequest, TicketQueryResponse,
|
||||||
truncate_body, validate_project_id,
|
TicketShowRequest, TicketSummary, summarize_body, truncate_body, validate_project_id,
|
||||||
};
|
};
|
||||||
use crate::store::{
|
use crate::store::{
|
||||||
ControlPlaneStore, MemoryDocumentRecord, MemoryStagingRecord, MemoryStagingResolutionRecord,
|
ControlPlaneStore, MemoryDocumentRecord, MemoryStagingRecord, MemoryStagingResolutionRecord,
|
||||||
@@ -163,6 +163,17 @@ impl SqliteWorkspaceAuthority {
|
|||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|link| link.ticket_id)
|
.map(|link| link.ticket_id)
|
||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
|
let linked_ticket_summaries = self
|
||||||
|
.list_tickets(1_000)?
|
||||||
|
.items
|
||||||
|
.into_iter()
|
||||||
|
.filter(|ticket| linked_tickets.iter().any(|id| id == &ticket.id))
|
||||||
|
.map(|ticket| ObjectiveLinkedTicketSummary {
|
||||||
|
id: ticket.id,
|
||||||
|
title: ticket.title,
|
||||||
|
state: ticket.state,
|
||||||
|
})
|
||||||
|
.collect::<Vec<_>>();
|
||||||
let resources = self
|
let resources = self
|
||||||
.store
|
.store
|
||||||
.list_objective_resources(&self.workspace_id, &record.objective_id)?
|
.list_objective_resources(&self.workspace_id, &record.objective_id)?
|
||||||
@@ -207,6 +218,7 @@ impl SqliteWorkspaceAuthority {
|
|||||||
created_at: Some(record.created_at),
|
created_at: Some(record.created_at),
|
||||||
updated_at: Some(record.updated_at),
|
updated_at: Some(record.updated_at),
|
||||||
linked_tickets,
|
linked_tickets,
|
||||||
|
linked_ticket_summaries,
|
||||||
resources,
|
resources,
|
||||||
body,
|
body,
|
||||||
body_truncated,
|
body_truncated,
|
||||||
@@ -295,6 +307,14 @@ impl SqliteWorkspaceAuthority {
|
|||||||
.filter(|(_, event)| event.kind.as_str() == "implementation_report")
|
.filter(|(_, event)| event.kind.as_str() == "implementation_report")
|
||||||
.map(|(sequence, event)| ticket_evidence_event(sequence, event))
|
.map(|(sequence, event)| ticket_evidence_event(sequence, event))
|
||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
|
let current_assignment = self
|
||||||
|
.store
|
||||||
|
.get_current_ticket_worker_assignment(&self.workspace_id, id)?
|
||||||
|
.map(|assignment| TicketAssignmentSummary {
|
||||||
|
assignment_id: assignment.assignment_id,
|
||||||
|
runtime_id: assignment.worker.runtime_id,
|
||||||
|
worker_id: assignment.worker.worker_id,
|
||||||
|
});
|
||||||
let merge_request = self
|
let merge_request = self
|
||||||
.merge_request_store
|
.merge_request_store
|
||||||
.show_for_ticket(id)
|
.show_for_ticket(id)
|
||||||
@@ -313,6 +333,7 @@ impl SqliteWorkspaceAuthority {
|
|||||||
id: ticket.meta.id,
|
id: ticket.meta.id,
|
||||||
title: ticket.meta.title,
|
title: ticket.meta.title,
|
||||||
state: ticket.meta.workflow_state.as_str().to_string(),
|
state: ticket.meta.workflow_state.as_str().to_string(),
|
||||||
|
readiness: ticket.meta.readiness,
|
||||||
priority: ticket.meta.priority,
|
priority: ticket.meta.priority,
|
||||||
created_at: ticket.meta.created_at,
|
created_at: ticket.meta.created_at,
|
||||||
updated_at: ticket.meta.updated_at,
|
updated_at: ticket.meta.updated_at,
|
||||||
@@ -345,6 +366,7 @@ impl SqliteWorkspaceAuthority {
|
|||||||
relations: ticket.relations.into(),
|
relations: ticket.relations.into(),
|
||||||
linked_objectives,
|
linked_objectives,
|
||||||
implementation_reports,
|
implementation_reports,
|
||||||
|
current_assignment,
|
||||||
merge_request,
|
merge_request,
|
||||||
evidence,
|
evidence,
|
||||||
resolution: ticket
|
resolution: ticket
|
||||||
@@ -388,7 +410,7 @@ impl TicketAuthority for SqliteWorkspaceAuthority {
|
|||||||
fn query_tickets(&self, query: TicketQueryRequest) -> Result<TicketQueryResponse> {
|
fn query_tickets(&self, query: TicketQueryRequest) -> Result<TicketQueryResponse> {
|
||||||
validate_ticket_query(&query)?;
|
validate_ticket_query(&query)?;
|
||||||
let limit = query.limit.unwrap_or(50).clamp(1, 100);
|
let limit = query.limit.unwrap_or(50).clamp(1, 100);
|
||||||
let sort = normalize_ticket_sort(query.sort.as_deref())?;
|
let sort = normalize_ticket_sort(query.sort.as_deref(), query.query.is_some())?;
|
||||||
let cursor = query
|
let cursor = query
|
||||||
.cursor
|
.cursor
|
||||||
.as_deref()
|
.as_deref()
|
||||||
@@ -457,6 +479,7 @@ impl ObjectiveAuthority for SqliteWorkspaceAuthority {
|
|||||||
id: record.objective_id,
|
id: record.objective_id,
|
||||||
title: record.title,
|
title: record.title,
|
||||||
state: record.state,
|
state: record.state,
|
||||||
|
created_at: Some(record.created_at),
|
||||||
updated_at: Some(record.updated_at),
|
updated_at: Some(record.updated_at),
|
||||||
summary: summarize_body(&record.body_md),
|
summary: summarize_body(&record.body_md),
|
||||||
linked_tickets,
|
linked_tickets,
|
||||||
@@ -476,7 +499,7 @@ impl ObjectiveAuthority for SqliteWorkspaceAuthority {
|
|||||||
query.updated_before.as_deref(),
|
query.updated_before.as_deref(),
|
||||||
)?;
|
)?;
|
||||||
let limit = query.limit.unwrap_or(50).clamp(1, 100);
|
let limit = query.limit.unwrap_or(50).clamp(1, 100);
|
||||||
let sort = normalize_objective_sort(query.sort.as_deref())?;
|
let sort = normalize_objective_sort(query.sort.as_deref(), query.query.is_some())?;
|
||||||
let cursor = query
|
let cursor = query
|
||||||
.cursor
|
.cursor
|
||||||
.as_deref()
|
.as_deref()
|
||||||
@@ -506,7 +529,7 @@ impl ObjectiveAuthority for SqliteWorkspaceAuthority {
|
|||||||
items.push(objective_query_item(
|
items.push(objective_query_item(
|
||||||
objective,
|
objective,
|
||||||
linked_tickets,
|
linked_tickets,
|
||||||
query.text.as_deref(),
|
query.query.as_deref(),
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
sort_objective_query_items(&mut items, sort);
|
sort_objective_query_items(&mut items, sort);
|
||||||
@@ -851,7 +874,9 @@ impl MemoryAuthority for SqliteWorkspaceAuthority {
|
|||||||
|
|
||||||
#[derive(Clone, Copy)]
|
#[derive(Clone, Copy)]
|
||||||
enum TicketQuerySort {
|
enum TicketQuerySort {
|
||||||
|
Relevance,
|
||||||
UpdatedDesc,
|
UpdatedDesc,
|
||||||
|
CreatedDesc,
|
||||||
Priority,
|
Priority,
|
||||||
Title,
|
Title,
|
||||||
}
|
}
|
||||||
@@ -859,7 +884,9 @@ enum TicketQuerySort {
|
|||||||
impl std::fmt::Display for TicketQuerySort {
|
impl std::fmt::Display for TicketQuerySort {
|
||||||
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
formatter.write_str(match self {
|
formatter.write_str(match self {
|
||||||
|
Self::Relevance => "relevance",
|
||||||
Self::UpdatedDesc => "updated_desc",
|
Self::UpdatedDesc => "updated_desc",
|
||||||
|
Self::CreatedDesc => "created_desc",
|
||||||
Self::Priority => "priority",
|
Self::Priority => "priority",
|
||||||
Self::Title => "title",
|
Self::Title => "title",
|
||||||
})
|
})
|
||||||
@@ -868,14 +895,18 @@ impl std::fmt::Display for TicketQuerySort {
|
|||||||
|
|
||||||
#[derive(Clone, Copy)]
|
#[derive(Clone, Copy)]
|
||||||
enum ObjectiveQuerySort {
|
enum ObjectiveQuerySort {
|
||||||
|
Relevance,
|
||||||
UpdatedDesc,
|
UpdatedDesc,
|
||||||
|
CreatedDesc,
|
||||||
Title,
|
Title,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl std::fmt::Display for ObjectiveQuerySort {
|
impl std::fmt::Display for ObjectiveQuerySort {
|
||||||
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
formatter.write_str(match self {
|
formatter.write_str(match self {
|
||||||
|
Self::Relevance => "relevance",
|
||||||
Self::UpdatedDesc => "updated_desc",
|
Self::UpdatedDesc => "updated_desc",
|
||||||
|
Self::CreatedDesc => "created_desc",
|
||||||
Self::Title => "title",
|
Self::Title => "title",
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -1031,7 +1062,13 @@ fn validate_ticket_query(query: &TicketQueryRequest) -> Result<()> {
|
|||||||
for attention in &query.attention {
|
for attention in &query.attention {
|
||||||
if !matches!(
|
if !matches!(
|
||||||
attention.as_str(),
|
attention.as_str(),
|
||||||
"blocked"
|
"done_not_closed"
|
||||||
|
| "implementation_report_not_closed"
|
||||||
|
| "report_after_rescope"
|
||||||
|
| "unresolved_review"
|
||||||
|
| "missing_commit"
|
||||||
|
| "blocked"
|
||||||
|
| "unblocked"
|
||||||
| "ready"
|
| "ready"
|
||||||
| "awaiting_review"
|
| "awaiting_review"
|
||||||
| "unresolved_changes"
|
| "unresolved_changes"
|
||||||
@@ -1044,7 +1081,15 @@ fn validate_ticket_query(query: &TicketQueryRequest) -> Result<()> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if let Some(status) = query.review_status.as_deref()
|
if let Some(status) = query.review_status.as_deref()
|
||||||
&& !matches!(status, "pending" | "approved" | "changes_requested")
|
&& !matches!(
|
||||||
|
status,
|
||||||
|
"none"
|
||||||
|
| "pending"
|
||||||
|
| "approved"
|
||||||
|
| "request_changes"
|
||||||
|
| "unresolved_changes"
|
||||||
|
| "changes_requested"
|
||||||
|
)
|
||||||
{
|
{
|
||||||
return Err(Error::InvalidRecordId(format!(
|
return Err(Error::InvalidRecordId(format!(
|
||||||
"unsupported review status `{status}`"
|
"unsupported review status `{status}`"
|
||||||
@@ -1080,9 +1125,15 @@ fn validate_time_bounds(after: Option<&str>, before: Option<&str>) -> Result<()>
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn normalize_ticket_sort(sort: Option<&str>) -> Result<TicketQuerySort> {
|
fn normalize_ticket_sort(sort: Option<&str>, has_query: bool) -> Result<TicketQuerySort> {
|
||||||
match sort.unwrap_or("updated_desc") {
|
match sort.unwrap_or(if has_query {
|
||||||
|
"relevance"
|
||||||
|
} else {
|
||||||
|
"updated_desc"
|
||||||
|
}) {
|
||||||
|
"relevance" => Ok(TicketQuerySort::Relevance),
|
||||||
"updated_desc" => Ok(TicketQuerySort::UpdatedDesc),
|
"updated_desc" => Ok(TicketQuerySort::UpdatedDesc),
|
||||||
|
"created_desc" => Ok(TicketQuerySort::CreatedDesc),
|
||||||
"priority" => Ok(TicketQuerySort::Priority),
|
"priority" => Ok(TicketQuerySort::Priority),
|
||||||
"title" => Ok(TicketQuerySort::Title),
|
"title" => Ok(TicketQuerySort::Title),
|
||||||
other => Err(Error::InvalidRecordId(format!(
|
other => Err(Error::InvalidRecordId(format!(
|
||||||
@@ -1091,9 +1142,15 @@ fn normalize_ticket_sort(sort: Option<&str>) -> Result<TicketQuerySort> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn normalize_objective_sort(sort: Option<&str>) -> Result<ObjectiveQuerySort> {
|
fn normalize_objective_sort(sort: Option<&str>, has_query: bool) -> Result<ObjectiveQuerySort> {
|
||||||
match sort.unwrap_or("updated_desc") {
|
match sort.unwrap_or(if has_query {
|
||||||
|
"relevance"
|
||||||
|
} else {
|
||||||
|
"updated_desc"
|
||||||
|
}) {
|
||||||
|
"relevance" => Ok(ObjectiveQuerySort::Relevance),
|
||||||
"updated_desc" => Ok(ObjectiveQuerySort::UpdatedDesc),
|
"updated_desc" => Ok(ObjectiveQuerySort::UpdatedDesc),
|
||||||
|
"created_desc" => Ok(ObjectiveQuerySort::CreatedDesc),
|
||||||
"title" => Ok(ObjectiveQuerySort::Title),
|
"title" => Ok(ObjectiveQuerySort::Title),
|
||||||
other => Err(Error::InvalidRecordId(format!(
|
other => Err(Error::InvalidRecordId(format!(
|
||||||
"unsupported Objective query sort `{other}`"
|
"unsupported Objective query sort `{other}`"
|
||||||
@@ -1154,11 +1211,18 @@ fn ticket_matches_query(
|
|||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if let Some(review_status) = &query.review_status
|
if let Some(review_status) = &query.review_status {
|
||||||
&& detail.evidence.review_status.as_ref() != Some(review_status)
|
let matches = match review_status.as_str() {
|
||||||
{
|
"none" => detail.evidence.review_status.is_none(),
|
||||||
|
"request_changes" | "unresolved_changes" => {
|
||||||
|
detail.evidence.review_status.as_deref() == Some("changes_requested")
|
||||||
|
}
|
||||||
|
status => detail.evidence.review_status.as_deref() == Some(status),
|
||||||
|
};
|
||||||
|
if !matches {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
if !query
|
if !query
|
||||||
.evidence
|
.evidence
|
||||||
.iter()
|
.iter()
|
||||||
@@ -1179,7 +1243,15 @@ fn ticket_matches_query(
|
|||||||
.attention
|
.attention
|
||||||
.iter()
|
.iter()
|
||||||
.all(|attention| match attention.as_str() {
|
.all(|attention| match attention.as_str() {
|
||||||
|
"done_not_closed" => summary.state == "done",
|
||||||
|
"implementation_report_not_closed" => {
|
||||||
|
detail.evidence.has_implementation_report && summary.state != "closed"
|
||||||
|
}
|
||||||
|
"report_after_rescope" => detail.evidence.implementation_report_after_rescope,
|
||||||
|
"unresolved_review" => detail.evidence.unresolved_request_changes,
|
||||||
|
"missing_commit" => !detail.evidence.has_commit,
|
||||||
"blocked" => !detail.relations.blockers.is_empty(),
|
"blocked" => !detail.relations.blockers.is_empty(),
|
||||||
|
"unblocked" => detail.relations.blockers.is_empty(),
|
||||||
"ready" => summary.state == "ready" && detail.relations.blockers.is_empty(),
|
"ready" => summary.state == "ready" && detail.relations.blockers.is_empty(),
|
||||||
"awaiting_review" => detail.evidence.review_status.as_deref() == Some("pending"),
|
"awaiting_review" => detail.evidence.review_status.as_deref() == Some("pending"),
|
||||||
"unresolved_changes" => detail.evidence.unresolved_request_changes,
|
"unresolved_changes" => detail.evidence.unresolved_request_changes,
|
||||||
@@ -1213,7 +1285,7 @@ fn ticket_matches_query(
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
query.text.as_ref().is_none_or(|text| {
|
query.query.as_ref().is_none_or(|text| {
|
||||||
let needle = text.to_lowercase();
|
let needle = text.to_lowercase();
|
||||||
summary.title.to_lowercase().contains(&needle)
|
summary.title.to_lowercase().contains(&needle)
|
||||||
|| detail.body.to_lowercase().contains(&needle)
|
|| detail.body.to_lowercase().contains(&needle)
|
||||||
@@ -1234,7 +1306,7 @@ fn ticket_query_item(
|
|||||||
let mut matched_fields = Vec::new();
|
let mut matched_fields = Vec::new();
|
||||||
let mut snippet = None;
|
let mut snippet = None;
|
||||||
let mut matching_event = None;
|
let mut matching_event = None;
|
||||||
if let Some(text) = query.text.as_ref() {
|
if let Some(text) = query.query.as_ref() {
|
||||||
let needle = text.to_lowercase();
|
let needle = text.to_lowercase();
|
||||||
if summary.title.to_lowercase().contains(&needle) {
|
if summary.title.to_lowercase().contains(&needle) {
|
||||||
matched_fields.push("title".to_string());
|
matched_fields.push("title".to_string());
|
||||||
@@ -1266,8 +1338,11 @@ fn ticket_query_item(
|
|||||||
id: summary.id,
|
id: summary.id,
|
||||||
title: summary.title,
|
title: summary.title,
|
||||||
state: summary.state,
|
state: summary.state,
|
||||||
|
readiness: detail.readiness.clone(),
|
||||||
priority: summary.priority,
|
priority: summary.priority,
|
||||||
|
created_at: detail.created_at.clone(),
|
||||||
updated_at: summary.updated_at,
|
updated_at: summary.updated_at,
|
||||||
|
item_revision: detail.item_revision.clone(),
|
||||||
workspace_action_priority: summary.workspace_action_priority,
|
workspace_action_priority: summary.workspace_action_priority,
|
||||||
matched_fields,
|
matched_fields,
|
||||||
snippet: snippet.map(|value| truncate_body(&value, 512).0),
|
snippet: snippet.map(|value| truncate_body(&value, 512).0),
|
||||||
@@ -1279,6 +1354,8 @@ fn ticket_query_item(
|
|||||||
.collect(),
|
.collect(),
|
||||||
relation_count: detail.relations.outgoing.len() + detail.relations.incoming.len(),
|
relation_count: detail.relations.outgoing.len() + detail.relations.incoming.len(),
|
||||||
blocker_count: detail.relations.blockers.len(),
|
blocker_count: detail.relations.blockers.len(),
|
||||||
|
unresolved_blocker_count: detail.relations.blockers.len(),
|
||||||
|
unresolved_review_count: usize::from(detail.evidence.unresolved_request_changes),
|
||||||
evidence: detail.evidence.clone(),
|
evidence: detail.evidence.clone(),
|
||||||
merge_request: detail.merge_request.clone(),
|
merge_request: detail.merge_request.clone(),
|
||||||
}
|
}
|
||||||
@@ -1295,9 +1372,27 @@ fn matching_snippet(body: &str, text: &str) -> String {
|
|||||||
body[start..end].to_string()
|
body[start..end].to_string()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn ticket_match_rank(item: &TicketQueryItem) -> usize {
|
||||||
|
if item.matched_fields.iter().any(|field| field == "title") {
|
||||||
|
0
|
||||||
|
} else if item.matched_fields.iter().any(|field| field == "body") {
|
||||||
|
1
|
||||||
|
} else if item.matched_fields.iter().any(|field| field == "event") {
|
||||||
|
2
|
||||||
|
} else {
|
||||||
|
3
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn ticket_sort_key(item: &TicketQueryItem, sort: TicketQuerySort) -> String {
|
fn ticket_sort_key(item: &TicketQueryItem, sort: TicketQuerySort) -> String {
|
||||||
match sort {
|
match sort {
|
||||||
|
TicketQuerySort::Relevance => format!(
|
||||||
|
"{}|{}",
|
||||||
|
ticket_match_rank(item),
|
||||||
|
item.updated_at.as_deref().unwrap_or("")
|
||||||
|
),
|
||||||
TicketQuerySort::UpdatedDesc => item.updated_at.clone().unwrap_or_default(),
|
TicketQuerySort::UpdatedDesc => item.updated_at.clone().unwrap_or_default(),
|
||||||
|
TicketQuerySort::CreatedDesc => item.created_at.clone().unwrap_or_default(),
|
||||||
TicketQuerySort::Priority => format!(
|
TicketQuerySort::Priority => format!(
|
||||||
"{}|{}",
|
"{}|{}",
|
||||||
match item.workspace_action_priority.as_str() {
|
match item.workspace_action_priority.as_str() {
|
||||||
@@ -1313,10 +1408,18 @@ fn ticket_sort_key(item: &TicketQueryItem, sort: TicketQuerySort) -> String {
|
|||||||
|
|
||||||
fn sort_ticket_query_items(items: &mut [TicketQueryItem], sort: TicketQuerySort) {
|
fn sort_ticket_query_items(items: &mut [TicketQueryItem], sort: TicketQuerySort) {
|
||||||
items.sort_by(|left, right| match sort {
|
items.sort_by(|left, right| match sort {
|
||||||
|
TicketQuerySort::Relevance => ticket_match_rank(left)
|
||||||
|
.cmp(&ticket_match_rank(right))
|
||||||
|
.then_with(|| right.updated_at.cmp(&left.updated_at))
|
||||||
|
.then_with(|| left.id.cmp(&right.id)),
|
||||||
TicketQuerySort::UpdatedDesc => right
|
TicketQuerySort::UpdatedDesc => right
|
||||||
.updated_at
|
.updated_at
|
||||||
.cmp(&left.updated_at)
|
.cmp(&left.updated_at)
|
||||||
.then_with(|| left.id.cmp(&right.id)),
|
.then_with(|| left.id.cmp(&right.id)),
|
||||||
|
TicketQuerySort::CreatedDesc => right
|
||||||
|
.created_at
|
||||||
|
.cmp(&left.created_at)
|
||||||
|
.then_with(|| left.id.cmp(&right.id)),
|
||||||
TicketQuerySort::Priority => {
|
TicketQuerySort::Priority => {
|
||||||
let rank = |item: &TicketQueryItem| match item.workspace_action_priority.as_str() {
|
let rank = |item: &TicketQueryItem| match item.workspace_action_priority.as_str() {
|
||||||
"ready_for_queue" => 0,
|
"ready_for_queue" => 0,
|
||||||
@@ -1347,9 +1450,11 @@ fn ticket_item_after_cursor(
|
|||||||
) -> bool {
|
) -> bool {
|
||||||
let key = ticket_sort_key(item, sort);
|
let key = ticket_sort_key(item, sort);
|
||||||
match sort {
|
match sort {
|
||||||
TicketQuerySort::UpdatedDesc => key < cursor.0 || (key == cursor.0 && item.id > cursor.1),
|
TicketQuerySort::UpdatedDesc | TicketQuerySort::CreatedDesc => {
|
||||||
|
key < cursor.0 || (key == cursor.0 && item.id > cursor.1)
|
||||||
|
}
|
||||||
TicketQuerySort::Title => key > cursor.0 || (key == cursor.0 && item.id > cursor.1),
|
TicketQuerySort::Title => key > cursor.0 || (key == cursor.0 && item.id > cursor.1),
|
||||||
TicketQuerySort::Priority => {
|
TicketQuerySort::Relevance | TicketQuerySort::Priority => {
|
||||||
let (rank, updated) = key.split_once('|').unwrap_or(("9", ""));
|
let (rank, updated) = key.split_once('|').unwrap_or(("9", ""));
|
||||||
let (cursor_rank, cursor_updated) = cursor.0.split_once('|').unwrap_or(("9", ""));
|
let (cursor_rank, cursor_updated) = cursor.0.split_once('|').unwrap_or(("9", ""));
|
||||||
rank > cursor_rank
|
rank > cursor_rank
|
||||||
@@ -1375,7 +1480,7 @@ fn objective_matches_query(objective: &ObjectiveSummary, query: &ObjectiveQueryR
|
|||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
query.text.as_ref().is_none_or(|text| {
|
query.query.as_ref().is_none_or(|text| {
|
||||||
let needle = text.to_lowercase();
|
let needle = text.to_lowercase();
|
||||||
objective.title.to_lowercase().contains(&needle)
|
objective.title.to_lowercase().contains(&needle)
|
||||||
|| objective.summary.to_lowercase().contains(&needle)
|
|| objective.summary.to_lowercase().contains(&needle)
|
||||||
@@ -1404,6 +1509,7 @@ fn objective_query_item(
|
|||||||
id: objective.id,
|
id: objective.id,
|
||||||
title: objective.title,
|
title: objective.title,
|
||||||
state: objective.state,
|
state: objective.state,
|
||||||
|
created_at: objective.created_at,
|
||||||
updated_at: objective.updated_at,
|
updated_at: objective.updated_at,
|
||||||
matched_fields,
|
matched_fields,
|
||||||
snippet,
|
snippet,
|
||||||
@@ -1412,19 +1518,43 @@ fn objective_query_item(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn objective_match_rank(item: &ObjectiveQueryItem) -> usize {
|
||||||
|
if item.matched_fields.iter().any(|field| field == "title") {
|
||||||
|
0
|
||||||
|
} else if item.matched_fields.iter().any(|field| field == "body") {
|
||||||
|
1
|
||||||
|
} else {
|
||||||
|
2
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn objective_sort_key(item: &ObjectiveQueryItem, sort: ObjectiveQuerySort) -> String {
|
fn objective_sort_key(item: &ObjectiveQueryItem, sort: ObjectiveQuerySort) -> String {
|
||||||
match sort {
|
match sort {
|
||||||
|
ObjectiveQuerySort::Relevance => format!(
|
||||||
|
"{}|{}",
|
||||||
|
objective_match_rank(item),
|
||||||
|
item.updated_at.as_deref().unwrap_or("")
|
||||||
|
),
|
||||||
ObjectiveQuerySort::UpdatedDesc => item.updated_at.clone().unwrap_or_default(),
|
ObjectiveQuerySort::UpdatedDesc => item.updated_at.clone().unwrap_or_default(),
|
||||||
|
ObjectiveQuerySort::CreatedDesc => item.created_at.clone().unwrap_or_default(),
|
||||||
ObjectiveQuerySort::Title => item.title.to_lowercase(),
|
ObjectiveQuerySort::Title => item.title.to_lowercase(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn sort_objective_query_items(items: &mut [ObjectiveQueryItem], sort: ObjectiveQuerySort) {
|
fn sort_objective_query_items(items: &mut [ObjectiveQueryItem], sort: ObjectiveQuerySort) {
|
||||||
items.sort_by(|left, right| match sort {
|
items.sort_by(|left, right| match sort {
|
||||||
|
ObjectiveQuerySort::Relevance => objective_match_rank(left)
|
||||||
|
.cmp(&objective_match_rank(right))
|
||||||
|
.then_with(|| right.updated_at.cmp(&left.updated_at))
|
||||||
|
.then_with(|| left.id.cmp(&right.id)),
|
||||||
ObjectiveQuerySort::UpdatedDesc => right
|
ObjectiveQuerySort::UpdatedDesc => right
|
||||||
.updated_at
|
.updated_at
|
||||||
.cmp(&left.updated_at)
|
.cmp(&left.updated_at)
|
||||||
.then_with(|| left.id.cmp(&right.id)),
|
.then_with(|| left.id.cmp(&right.id)),
|
||||||
|
ObjectiveQuerySort::CreatedDesc => right
|
||||||
|
.created_at
|
||||||
|
.cmp(&left.created_at)
|
||||||
|
.then_with(|| left.id.cmp(&right.id)),
|
||||||
ObjectiveQuerySort::Title => left
|
ObjectiveQuerySort::Title => left
|
||||||
.title
|
.title
|
||||||
.to_lowercase()
|
.to_lowercase()
|
||||||
@@ -1444,9 +1574,17 @@ fn objective_item_after_cursor(
|
|||||||
) -> bool {
|
) -> bool {
|
||||||
let key = objective_sort_key(item, sort);
|
let key = objective_sort_key(item, sort);
|
||||||
match sort {
|
match sort {
|
||||||
ObjectiveQuerySort::UpdatedDesc => {
|
ObjectiveQuerySort::UpdatedDesc | ObjectiveQuerySort::CreatedDesc => {
|
||||||
key < cursor.0 || (key == cursor.0 && item.id > cursor.1)
|
key < cursor.0 || (key == cursor.0 && item.id > cursor.1)
|
||||||
}
|
}
|
||||||
|
ObjectiveQuerySort::Relevance => {
|
||||||
|
let (rank, updated) = key.split_once('|').unwrap_or(("9", ""));
|
||||||
|
let (cursor_rank, cursor_updated) = cursor.0.split_once('|').unwrap_or(("9", ""));
|
||||||
|
rank > cursor_rank
|
||||||
|
|| (rank == cursor_rank
|
||||||
|
&& (updated < cursor_updated
|
||||||
|
|| (updated == cursor_updated && item.id > cursor.1)))
|
||||||
|
}
|
||||||
ObjectiveQuerySort::Title => key > cursor.0 || (key == cursor.0 && item.id > cursor.1),
|
ObjectiveQuerySort::Title => key > cursor.0 || (key == cursor.0 && item.id > cursor.1),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1696,9 +1834,10 @@ mod tests {
|
|||||||
assert_eq!(ticket.event_page.returned, ticket.events.len());
|
assert_eq!(ticket.event_page.returned, ticket.events.len());
|
||||||
let ticket_query = authority
|
let ticket_query = authority
|
||||||
.query_tickets(TicketQueryRequest {
|
.query_tickets(TicketQueryRequest {
|
||||||
text: Some("Ticket body".to_string()),
|
query: Some("Ticket body".to_string()),
|
||||||
states: vec!["ready".to_string()],
|
states: vec!["ready".to_string()],
|
||||||
linked_objective_id: Some("00000000001J3".to_string()),
|
linked_objective_id: Some("00000000001J3".to_string()),
|
||||||
|
attention: vec!["unblocked".to_string(), "missing_commit".to_string()],
|
||||||
limit: Some(1),
|
limit: Some(1),
|
||||||
..TicketQueryRequest::default()
|
..TicketQueryRequest::default()
|
||||||
})
|
})
|
||||||
@@ -1740,9 +1879,11 @@ mod tests {
|
|||||||
let objective = authority.objective("00000000001J3").unwrap();
|
let objective = authority.objective("00000000001J3").unwrap();
|
||||||
assert!(objective.body.contains("Objective body"));
|
assert!(objective.body.contains("Objective body"));
|
||||||
assert!(!objective.revision.is_empty());
|
assert!(!objective.revision.is_empty());
|
||||||
|
assert_eq!(objective.linked_ticket_summaries[0].id, "00000000001J2");
|
||||||
|
assert_eq!(objective.linked_ticket_summaries[0].state, "ready");
|
||||||
let objective_query = authority
|
let objective_query = authority
|
||||||
.query_objectives(ObjectiveQueryRequest {
|
.query_objectives(ObjectiveQueryRequest {
|
||||||
text: Some("Control plane".to_string()),
|
query: Some("Control plane".to_string()),
|
||||||
linked_ticket_id: Some("00000000001J2".to_string()),
|
linked_ticket_id: Some("00000000001J2".to_string()),
|
||||||
limit: Some(1),
|
limit: Some(1),
|
||||||
..ObjectiveQueryRequest::default()
|
..ObjectiveQueryRequest::default()
|
||||||
|
|||||||
@@ -49,6 +49,7 @@ pub struct TicketDetail {
|
|||||||
pub id: String,
|
pub id: String,
|
||||||
pub title: String,
|
pub title: String,
|
||||||
pub state: String,
|
pub state: String,
|
||||||
|
pub readiness: Option<String>,
|
||||||
pub priority: String,
|
pub priority: String,
|
||||||
pub created_at: Option<String>,
|
pub created_at: Option<String>,
|
||||||
pub updated_at: Option<String>,
|
pub updated_at: Option<String>,
|
||||||
@@ -69,6 +70,7 @@ pub struct TicketDetail {
|
|||||||
pub relations: TicketRelationView,
|
pub relations: TicketRelationView,
|
||||||
pub linked_objectives: Vec<ObjectiveLinkSummary>,
|
pub linked_objectives: Vec<ObjectiveLinkSummary>,
|
||||||
pub implementation_reports: Vec<TicketEvidenceEvent>,
|
pub implementation_reports: Vec<TicketEvidenceEvent>,
|
||||||
|
pub current_assignment: Option<TicketAssignmentSummary>,
|
||||||
pub merge_request: Option<TicketMergeRequestSummary>,
|
pub merge_request: Option<TicketMergeRequestSummary>,
|
||||||
pub evidence: TicketEvidenceSummary,
|
pub evidence: TicketEvidenceSummary,
|
||||||
pub resolution: Option<String>,
|
pub resolution: Option<String>,
|
||||||
@@ -225,6 +227,14 @@ pub struct TicketEvidenceEvent {
|
|||||||
pub excerpt: String,
|
pub excerpt: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||||
|
#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
|
||||||
|
pub struct TicketAssignmentSummary {
|
||||||
|
pub assignment_id: String,
|
||||||
|
pub runtime_id: String,
|
||||||
|
pub worker_id: String,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||||
#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
|
#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
|
||||||
pub struct TicketMergeRequestSummary {
|
pub struct TicketMergeRequestSummary {
|
||||||
@@ -257,7 +267,7 @@ pub struct TicketEvidenceSummary {
|
|||||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Default)]
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Default)]
|
||||||
#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
|
#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
|
||||||
pub struct TicketQueryRequest {
|
pub struct TicketQueryRequest {
|
||||||
pub text: Option<String>,
|
pub query: Option<String>,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub states: Vec<String>,
|
pub states: Vec<String>,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
@@ -283,8 +293,11 @@ pub struct TicketQueryItem {
|
|||||||
pub id: String,
|
pub id: String,
|
||||||
pub title: String,
|
pub title: String,
|
||||||
pub state: String,
|
pub state: String,
|
||||||
|
pub readiness: Option<String>,
|
||||||
pub priority: String,
|
pub priority: String,
|
||||||
|
pub created_at: Option<String>,
|
||||||
pub updated_at: Option<String>,
|
pub updated_at: Option<String>,
|
||||||
|
pub item_revision: String,
|
||||||
pub workspace_action_priority: String,
|
pub workspace_action_priority: String,
|
||||||
pub matched_fields: Vec<String>,
|
pub matched_fields: Vec<String>,
|
||||||
pub snippet: Option<String>,
|
pub snippet: Option<String>,
|
||||||
@@ -292,6 +305,8 @@ pub struct TicketQueryItem {
|
|||||||
pub linked_objective_ids: Vec<String>,
|
pub linked_objective_ids: Vec<String>,
|
||||||
pub relation_count: usize,
|
pub relation_count: usize,
|
||||||
pub blocker_count: usize,
|
pub blocker_count: usize,
|
||||||
|
pub unresolved_blocker_count: usize,
|
||||||
|
pub unresolved_review_count: usize,
|
||||||
pub evidence: TicketEvidenceSummary,
|
pub evidence: TicketEvidenceSummary,
|
||||||
pub merge_request: Option<TicketMergeRequestSummary>,
|
pub merge_request: Option<TicketMergeRequestSummary>,
|
||||||
}
|
}
|
||||||
@@ -313,7 +328,7 @@ pub struct TicketShowRequest {
|
|||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Default)]
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Default)]
|
||||||
pub struct ObjectiveQueryRequest {
|
pub struct ObjectiveQueryRequest {
|
||||||
pub text: Option<String>,
|
pub query: Option<String>,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub states: Vec<String>,
|
pub states: Vec<String>,
|
||||||
pub linked_ticket_id: Option<String>,
|
pub linked_ticket_id: Option<String>,
|
||||||
@@ -329,6 +344,7 @@ pub struct ObjectiveQueryItem {
|
|||||||
pub id: String,
|
pub id: String,
|
||||||
pub title: String,
|
pub title: String,
|
||||||
pub state: String,
|
pub state: String,
|
||||||
|
pub created_at: Option<String>,
|
||||||
pub updated_at: Option<String>,
|
pub updated_at: Option<String>,
|
||||||
pub matched_fields: Vec<String>,
|
pub matched_fields: Vec<String>,
|
||||||
pub snippet: Option<String>,
|
pub snippet: Option<String>,
|
||||||
@@ -358,11 +374,19 @@ pub struct ObjectiveEventDetail {
|
|||||||
pub created_at: String,
|
pub created_at: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||||
|
pub struct ObjectiveLinkedTicketSummary {
|
||||||
|
pub id: String,
|
||||||
|
pub title: String,
|
||||||
|
pub state: String,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||||
pub struct ObjectiveSummary {
|
pub struct ObjectiveSummary {
|
||||||
pub id: String,
|
pub id: String,
|
||||||
pub title: String,
|
pub title: String,
|
||||||
pub state: String,
|
pub state: String,
|
||||||
|
pub created_at: Option<String>,
|
||||||
pub updated_at: Option<String>,
|
pub updated_at: Option<String>,
|
||||||
pub summary: String,
|
pub summary: String,
|
||||||
pub linked_tickets: Vec<String>,
|
pub linked_tickets: Vec<String>,
|
||||||
@@ -378,6 +402,7 @@ pub struct ObjectiveDetail {
|
|||||||
pub created_at: Option<String>,
|
pub created_at: Option<String>,
|
||||||
pub updated_at: Option<String>,
|
pub updated_at: Option<String>,
|
||||||
pub linked_tickets: Vec<String>,
|
pub linked_tickets: Vec<String>,
|
||||||
|
pub linked_ticket_summaries: Vec<ObjectiveLinkedTicketSummary>,
|
||||||
pub resources: Vec<ObjectiveResourceSummary>,
|
pub resources: Vec<ObjectiveResourceSummary>,
|
||||||
pub body: String,
|
pub body: String,
|
||||||
pub body_truncated: bool,
|
pub body_truncated: bool,
|
||||||
@@ -407,6 +432,7 @@ pub fn ticket_api_typescript() -> String {
|
|||||||
TicketEventDetail::decl(&config),
|
TicketEventDetail::decl(&config),
|
||||||
ObjectiveLinkSummary::decl(&config),
|
ObjectiveLinkSummary::decl(&config),
|
||||||
TicketEvidenceEvent::decl(&config),
|
TicketEvidenceEvent::decl(&config),
|
||||||
|
TicketAssignmentSummary::decl(&config),
|
||||||
TicketMergeRequestSummary::decl(&config),
|
TicketMergeRequestSummary::decl(&config),
|
||||||
TicketEvidenceSummary::decl(&config),
|
TicketEvidenceSummary::decl(&config),
|
||||||
TicketQueryRequest::decl(&config),
|
TicketQueryRequest::decl(&config),
|
||||||
|
|||||||
@@ -17799,7 +17799,7 @@ mod tests {
|
|||||||
"POST",
|
"POST",
|
||||||
&format!("/api/w/{TEST_WORKSPACE_ID}/objectives/query"),
|
&format!("/api/w/{TEST_WORKSPACE_ID}/objectives/query"),
|
||||||
Some(json!({
|
Some(json!({
|
||||||
"text": "Objective body",
|
"query": "Objective body",
|
||||||
"linked_ticket_id": "00000000001J2",
|
"linked_ticket_id": "00000000001J2",
|
||||||
"limit": 1
|
"limit": 1
|
||||||
})),
|
})),
|
||||||
|
|||||||
@@ -41,8 +41,8 @@ Workers with the Ticket built-in feature can use typed Ticket tools:
|
|||||||
- `MergeRequestReviewSubmit` — available only inside the attested direct-child Reviewer attempt; attempt/revision capability material is not model input.
|
- `MergeRequestReviewSubmit` — available only inside the attested direct-child Reviewer attempt; attempt/revision capability material is not model input.
|
||||||
- `TicketClose`
|
- `TicketClose`
|
||||||
- `TicketRelationRecord`
|
- `TicketRelationRecord`
|
||||||
- `TicketRelationQuery`
|
|
||||||
- `TicketDoctor`
|
Profile-visible Ticket catalogs are intentionally smaller than the former broad read catalog: Workspace authoring exposes 9 tools instead of 13, workflow exposes 10 instead of 12, and review exposes only `QueryTicket` plus `ShowTicket` (2 instead of 6). The `QueryTicket` schema is regression-guarded below 8 KiB while consolidating relation/evidence/attention discovery; diagnostics are not projected into normal profiles, while specialized orchestration-plan commands remain visible only to workflow roles that need their distinct semantics.
|
||||||
|
|
||||||
These tools operate through the typed Ticket backend. They are not arbitrary filesystem write permission to `.yoi/tickets/`.
|
These tools operate through the typed Ticket backend. They are not arbitrary filesystem write permission to `.yoi/tickets/`.
|
||||||
|
|
||||||
|
|||||||
@@ -61,6 +61,12 @@ export type TicketEvidenceEvent = {
|
|||||||
excerpt: string;
|
excerpt: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type TicketAssignmentSummary = {
|
||||||
|
assignment_id: string;
|
||||||
|
runtime_id: string;
|
||||||
|
worker_id: string;
|
||||||
|
};
|
||||||
|
|
||||||
export type TicketMergeRequestSummary = {
|
export type TicketMergeRequestSummary = {
|
||||||
merge_request_id: string;
|
merge_request_id: string;
|
||||||
state: string;
|
state: string;
|
||||||
@@ -87,7 +93,7 @@ export type TicketEvidenceSummary = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export type TicketQueryRequest = {
|
export type TicketQueryRequest = {
|
||||||
text: string | null;
|
query: string | null;
|
||||||
states: Array<string>;
|
states: Array<string>;
|
||||||
event_kinds: Array<string>;
|
event_kinds: Array<string>;
|
||||||
evidence: Array<string>;
|
evidence: Array<string>;
|
||||||
@@ -107,8 +113,11 @@ export type TicketQueryItem = {
|
|||||||
id: string;
|
id: string;
|
||||||
title: string;
|
title: string;
|
||||||
state: string;
|
state: string;
|
||||||
|
readiness: string | null;
|
||||||
priority: string;
|
priority: string;
|
||||||
|
created_at: string | null;
|
||||||
updated_at: string | null;
|
updated_at: string | null;
|
||||||
|
item_revision: string;
|
||||||
workspace_action_priority: string;
|
workspace_action_priority: string;
|
||||||
matched_fields: Array<string>;
|
matched_fields: Array<string>;
|
||||||
snippet: string | null;
|
snippet: string | null;
|
||||||
@@ -116,6 +125,8 @@ export type TicketQueryItem = {
|
|||||||
linked_objective_ids: Array<string>;
|
linked_objective_ids: Array<string>;
|
||||||
relation_count: number;
|
relation_count: number;
|
||||||
blocker_count: number;
|
blocker_count: number;
|
||||||
|
unresolved_blocker_count: number;
|
||||||
|
unresolved_review_count: number;
|
||||||
evidence: TicketEvidenceSummary;
|
evidence: TicketEvidenceSummary;
|
||||||
merge_request: TicketMergeRequestSummary | null;
|
merge_request: TicketMergeRequestSummary | null;
|
||||||
};
|
};
|
||||||
@@ -174,6 +185,7 @@ export type TicketDetail = {
|
|||||||
id: string;
|
id: string;
|
||||||
title: string;
|
title: string;
|
||||||
state: string;
|
state: string;
|
||||||
|
readiness: string | null;
|
||||||
priority: string;
|
priority: string;
|
||||||
created_at: string | null;
|
created_at: string | null;
|
||||||
updated_at: string | null;
|
updated_at: string | null;
|
||||||
@@ -194,6 +206,7 @@ export type TicketDetail = {
|
|||||||
relations: TicketRelationView;
|
relations: TicketRelationView;
|
||||||
linked_objectives: Array<ObjectiveLinkSummary>;
|
linked_objectives: Array<ObjectiveLinkSummary>;
|
||||||
implementation_reports: Array<TicketEvidenceEvent>;
|
implementation_reports: Array<TicketEvidenceEvent>;
|
||||||
|
current_assignment: TicketAssignmentSummary | null;
|
||||||
merge_request: TicketMergeRequestSummary | null;
|
merge_request: TicketMergeRequestSummary | null;
|
||||||
evidence: TicketEvidenceSummary;
|
evidence: TicketEvidenceSummary;
|
||||||
resolution: string | null;
|
resolution: string | null;
|
||||||
|
|||||||
Reference in New Issue
Block a user