Add tool quality report contract
Deploy / deploy (push) Successful in 1m40s
CI / Rust Checks (push) Failing after 4m48s
CI / Frontend E2E (push) Has been skipped
CI / Deployment Manifests (push) Has been skipped
CI / UI Checks (push) Has been skipped

This commit is contained in:
github-ops
2026-06-20 19:50:46 +00:00
parent 906b6c1aea
commit 3b61d7a25d
8 changed files with 110 additions and 4 deletions
+8 -4
View File
@@ -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}",
+12
View File
@@ -64,6 +64,18 @@ pub async fn create_operation(
Ok(Json(json!(created)))
}
pub async fn analyze_operation_quality(
Path(path): Path<WorkspacePath>,
State(state): State<AppState>,
Json(payload): Json<OperationPayload>,
) -> Result<Json<Value>, 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<WorkspaceOperationPath>,
State(state): State<AppState>,
+12
View File
@@ -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<crank_core::ToolQualityReport, ApiError> {
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,
+2
View File
@@ -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};
+40
View File
@@ -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<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub field_path: Option<String>,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct ToolQualityReport {
pub blocking: bool,
pub findings: Vec<ToolQualityFinding>,
}
impl ToolQualityReport {
pub fn new(findings: Vec<ToolQualityFinding>) -> Self {
let blocking = findings
.iter()
.any(|finding| finding.severity == ToolQualitySeverity::Error);
Self { blocking, findings }
}
pub fn empty() -> Self {
Self::new(Vec::new())
}
}
+3
View File
@@ -0,0 +1,3 @@
mod unit {
mod tool_quality;
}
@@ -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);
}
+1
View File
@@ -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}`