diff --git a/apps/admin-api/src/app.rs b/apps/admin-api/src/app.rs index 220d74d..6866ebe 100644 --- a/apps/admin-api/src/app.rs +++ b/apps/admin-api/src/app.rs @@ -20,10 +20,10 @@ use crate::{ capabilities::get_capabilities, observability::{get_agent_usage, get_log, get_operation_usage, get_usage, list_logs}, operations::{ - archive_operation, create_operation, create_version, delete_operation, - export_operation, generate_draft, get_operation, get_operation_version, - list_operations, publish_operation, run_test, update_operation, upload_input_json, - upload_output_json, + analyze_operation_quality, archive_operation, create_operation, create_version, + delete_operation, export_operation, generate_draft, get_operation, + get_operation_version, list_operations, publish_operation, run_test, update_operation, + upload_input_json, upload_output_json, }, secrets::{create_secret, delete_secret, get_secret, list_secrets, rotate_secret}, upstreams::{create_upstream, list_upstreams, update_upstream}, @@ -35,6 +35,10 @@ use crate::{ pub fn build_app(state: AppState) -> Router { let workspace_router = Router::new() .route("/operations", get(list_operations).post(create_operation)) + .route( + "/operations/analyze-quality", + post(analyze_operation_quality), + ) .route("/operations/import", post(import_operation)) .route( "/operations/{operation_id}", diff --git a/apps/admin-api/src/routes/operations.rs b/apps/admin-api/src/routes/operations.rs index 49d6b4c..8c5e8f9 100644 --- a/apps/admin-api/src/routes/operations.rs +++ b/apps/admin-api/src/routes/operations.rs @@ -64,6 +64,18 @@ pub async fn create_operation( Ok(Json(json!(created))) } +pub async fn analyze_operation_quality( + Path(path): Path, + State(state): State, + Json(payload): Json, +) -> Result, ApiError> { + let report = state + .service + .analyze_operation_quality(&path.workspace_id.as_str().into(), payload) + .await?; + Ok(Json(json!(report))) +} + pub async fn get_operation( Path(path): Path, State(state): State, diff --git a/apps/admin-api/src/service.rs b/apps/admin-api/src/service.rs index 8bba890..1309465 100644 --- a/apps/admin-api/src/service.rs +++ b/apps/admin-api/src/service.rs @@ -1443,6 +1443,18 @@ impl AdminService { }) } + #[instrument(skip(self, payload), fields(protocol = ?payload.protocol, operation_name = %payload.name))] + pub async fn analyze_operation_quality( + &self, + workspace_id: &WorkspaceId, + payload: OperationPayload, + ) -> Result { + self.validate_operation_payload(&payload)?; + self.ensure_workspace_exists(workspace_id).await?; + + Ok(crank_core::ToolQualityReport::empty()) + } + #[instrument(skip(self, payload), fields(operation_id = %operation_id.as_str(), protocol = ?payload.operation.protocol))] pub async fn create_version( &self, diff --git a/crates/crank-core/src/lib.rs b/crates/crank-core/src/lib.rs index 938a884..f84675c 100644 --- a/crates/crank-core/src/lib.rs +++ b/crates/crank-core/src/lib.rs @@ -9,6 +9,7 @@ pub mod observability; pub mod operation; pub mod protocol; pub mod secret; +pub mod tool_quality; pub mod workspace; pub use access::{ @@ -61,4 +62,5 @@ pub use operation::{ }; pub use protocol::{AuthKind, ExportMode, HttpMethod, Protocol}; pub use secret::{Secret, SecretKind, SecretStatus, SecretVersion}; +pub use tool_quality::{ToolQualityFinding, ToolQualityReport, ToolQualitySeverity}; pub use workspace::{Workspace, WorkspaceStatus}; diff --git a/crates/crank-core/src/tool_quality.rs b/crates/crank-core/src/tool_quality.rs new file mode 100644 index 0000000..07ef5cd --- /dev/null +++ b/crates/crank-core/src/tool_quality.rs @@ -0,0 +1,40 @@ +use serde::{Deserialize, Serialize}; + +#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "snake_case")] +pub enum ToolQualitySeverity { + Info, + Warning, + Error, +} + +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +pub struct ToolQualityFinding { + pub severity: ToolQualitySeverity, + pub code: String, + pub message: String, + #[serde(skip_serializing_if = "Option::is_none")] + pub suggested_action: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub field_path: Option, +} + +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +pub struct ToolQualityReport { + pub blocking: bool, + pub findings: Vec, +} + +impl ToolQualityReport { + pub fn new(findings: Vec) -> Self { + let blocking = findings + .iter() + .any(|finding| finding.severity == ToolQualitySeverity::Error); + + Self { blocking, findings } + } + + pub fn empty() -> Self { + Self::new(Vec::new()) + } +} diff --git a/crates/crank-core/tests/unit.rs b/crates/crank-core/tests/unit.rs new file mode 100644 index 0000000..56fbfe8 --- /dev/null +++ b/crates/crank-core/tests/unit.rs @@ -0,0 +1,3 @@ +mod unit { + mod tool_quality; +} diff --git a/crates/crank-core/tests/unit/tool_quality.rs b/crates/crank-core/tests/unit/tool_quality.rs new file mode 100644 index 0000000..29d3a63 --- /dev/null +++ b/crates/crank-core/tests/unit/tool_quality.rs @@ -0,0 +1,32 @@ +use crank_core::{ToolQualityFinding, ToolQualityReport, ToolQualitySeverity}; + +#[test] +fn serializes_tool_quality_finding_contract() { + let finding = ToolQualityFinding { + severity: ToolQualitySeverity::Warning, + code: "tool_description_too_short".to_owned(), + message: "Описание инструмента слишком короткое.".to_owned(), + suggested_action: Some("Добавьте назначение и условия применения инструмента.".to_owned()), + field_path: Some("tool_description.description".to_owned()), + }; + + let value = serde_json::to_value(finding).unwrap(); + + assert_eq!(value["severity"], "warning"); + assert_eq!(value["code"], "tool_description_too_short"); + assert_eq!(value["field_path"], "tool_description.description"); +} + +#[test] +fn report_is_blocking_when_error_finding_exists() { + let report = ToolQualityReport::new(vec![ToolQualityFinding { + severity: ToolQualitySeverity::Error, + code: "tool_name_missing".to_owned(), + message: "Имя инструмента обязательно.".to_owned(), + suggested_action: None, + field_path: Some("name".to_owned()), + }]); + + assert!(report.blocking); + assert_eq!(report.findings.len(), 1); +} diff --git a/docs/admin-api.md b/docs/admin-api.md index e8a1bd7..5551824 100644 --- a/docs/admin-api.md +++ b/docs/admin-api.md @@ -43,6 +43,7 @@ Community работает с одним bootstrap workspace. API не подд - `GET /api/admin/workspaces/{workspace_id}/operations` - `POST /api/admin/workspaces/{workspace_id}/operations` +- `POST /api/admin/workspaces/{workspace_id}/operations/analyze-quality` - `GET /api/admin/workspaces/{workspace_id}/operations/{operation_id}` - `PATCH /api/admin/workspaces/{workspace_id}/operations/{operation_id}` - `DELETE /api/admin/workspaces/{workspace_id}/operations/{operation_id}`