From a758327e181c9d536f7a29d7c0fc9b8639010ef3 Mon Sep 17 00:00:00 2001 From: ITSGJY <133723203+ITSGJY@users.noreply.github.com> Date: Thu, 1 Oct 2026 12:06:10 +0800 Subject: [PATCH] refactor(project): share project operation scope --- crates/webcodex-core/src/lib.rs | 4 ++ crates/webcodex-core/src/project_build.rs | 37 ++++------ crates/webcodex-core/src/project_operation.rs | 38 ++++++++++ .../src/project_operation_tests.rs | 71 +++++++++++++++++++ .../webcodex-core/src/project_validation.rs | 36 ++++------ 5 files changed, 137 insertions(+), 49 deletions(-) create mode 100644 crates/webcodex-core/src/project_operation.rs create mode 100644 crates/webcodex-core/src/project_operation_tests.rs diff --git a/crates/webcodex-core/src/lib.rs b/crates/webcodex-core/src/lib.rs index d6da077d6..e962ef131 100644 --- a/crates/webcodex-core/src/lib.rs +++ b/crates/webcodex-core/src/lib.rs @@ -22,6 +22,7 @@ pub mod project_build; pub mod project_context_contract; pub mod project_instructions; pub mod project_listing; +mod project_operation; pub mod runner_instruction; pub mod runner_job_lifecycle; pub mod runner_job_receipt; @@ -51,3 +52,6 @@ mod project_validation_tests; #[cfg(test)] mod project_build_tests; + +#[cfg(test)] +mod project_operation_tests; diff --git a/crates/webcodex-core/src/project_build.rs b/crates/webcodex-core/src/project_build.rs index 78d1f659a..104407869 100644 --- a/crates/webcodex-core/src/project_build.rs +++ b/crates/webcodex-core/src/project_build.rs @@ -1,7 +1,11 @@ //! Declarative project build planning and admission provenance. No model argv. +use crate::project_operation::ProjectOperationScopeError; use crate::runner_protocol::{normalize_cargo_packages, normalize_go_packages, ShellProcessArgv}; use serde::{Deserialize, Serialize}; + +/// Portable project-operation scope retained under the project_build API name. +pub use crate::project_operation::ProjectOperationScope as ProjectBuildScope; use sha2::{Digest, Sha256}; pub const PROJECT_BUILD_PROVENANCE_MAX_BYTES: usize = 8 * 1024; @@ -17,30 +21,6 @@ pub enum ProjectBuildAdapter { Go, } -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)] -#[serde(deny_unknown_fields)] -pub struct ProjectBuildScope { - /// Portable bounded package scope. Rust maps values to repeated Cargo -p - /// selectors; Go maps values to project-relative package patterns. - #[schemars(length(min = 1, max = 8))] - #[schemars(inner(length(min = 1, max = 256)))] - pub packages: Vec, -} - -impl ProjectBuildScope { - fn validate(&self) -> Result<(), String> { - if self.packages.is_empty() || self.packages.len() > 8 { - return Err("project build packages must contain between 1 and 8 items".into()); - } - if self.packages.iter().any(|package| { - package.is_empty() || package.len() > 256 || package.chars().any(char::is_control) - }) { - return Err("invalid project build package scope".into()); - } - Ok(()) - } -} - #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] #[serde(deny_unknown_fields)] pub struct ProjectBuildRequest { @@ -62,7 +42,14 @@ impl ProjectBuildRequest { } validate_relative(self.cwd.as_deref().unwrap_or("."))?; if let Some(scope) = &self.scope { - scope.validate()?; + scope.validate().map_err(|error| match error { + ProjectOperationScopeError::PackageCount => { + "project build packages must contain between 1 and 8 items".to_string() + } + ProjectOperationScopeError::InvalidPackage => { + "invalid project build package scope".to_string() + } + })?; } Ok(()) } diff --git a/crates/webcodex-core/src/project_operation.rs b/crates/webcodex-core/src/project_operation.rs new file mode 100644 index 000000000..d120e5132 --- /dev/null +++ b/crates/webcodex-core/src/project_operation.rs @@ -0,0 +1,38 @@ +//! Shared declarative scope for portable project lifecycle operations. + +use serde::{Deserialize, Serialize}; + +pub(crate) const PROJECT_OPERATION_SCOPE_MAX_PACKAGES: usize = 8; +pub(crate) const PROJECT_OPERATION_SCOPE_MAX_PACKAGE_BYTES: usize = 256; + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)] +#[serde(deny_unknown_fields)] +pub struct ProjectOperationScope { + /// Portable bounded package selectors. Adapters translate these semantic + /// selectors into ecosystem-specific package mechanics. + #[schemars(length(min = 1, max = 8))] + #[schemars(inner(length(min = 1, max = 256)))] + pub packages: Vec, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(crate) enum ProjectOperationScopeError { + PackageCount, + InvalidPackage, +} + +impl ProjectOperationScope { + pub(crate) fn validate(&self) -> Result<(), ProjectOperationScopeError> { + if self.packages.is_empty() || self.packages.len() > PROJECT_OPERATION_SCOPE_MAX_PACKAGES { + return Err(ProjectOperationScopeError::PackageCount); + } + if self.packages.iter().any(|package| { + package.is_empty() + || package.len() > PROJECT_OPERATION_SCOPE_MAX_PACKAGE_BYTES + || package.chars().any(char::is_control) + }) { + return Err(ProjectOperationScopeError::InvalidPackage); + } + Ok(()) + } +} diff --git a/crates/webcodex-core/src/project_operation_tests.rs b/crates/webcodex-core/src/project_operation_tests.rs new file mode 100644 index 000000000..4eb2a7322 --- /dev/null +++ b/crates/webcodex-core/src/project_operation_tests.rs @@ -0,0 +1,71 @@ +use crate::project_build::{ProjectBuildAdapter, ProjectBuildRequest, ProjectBuildScope}; +use crate::project_operation::ProjectOperationScope; +use crate::project_validation::{ + ProjectValidationAction, ProjectValidationAdapter, ProjectValidationRequest, + ProjectValidationScope, +}; + +fn build_request(scope: ProjectBuildScope) -> ProjectBuildRequest { + ProjectBuildRequest { + project_id: "demo".into(), + cwd: None, + adapter: ProjectBuildAdapter::Auto, + scope: Some(scope), + } +} + +fn validation_request(scope: ProjectValidationScope) -> ProjectValidationRequest { + ProjectValidationRequest { + project_id: "demo".into(), + cwd: None, + action: ProjectValidationAction::Check, + adapter: ProjectValidationAdapter::Auto, + scope: Some(scope), + test: None, + } +} + +#[test] +fn project_build_and_validation_scope_names_share_one_wire_type() { + let shared = ProjectOperationScope { + packages: vec!["package-a".into(), "package-b".into()], + }; + let build: ProjectBuildScope = shared.clone(); + let validation: ProjectValidationScope = shared.clone(); + + assert_eq!(build, validation); + assert_eq!( + serde_json::to_value(build).unwrap(), + serde_json::json!({"packages":["package-a","package-b"]}) + ); + assert_eq!( + serde_json::to_value(validation).unwrap(), + serde_json::json!({"packages":["package-a","package-b"]}) + ); +} + +#[test] +fn shared_scope_validation_preserves_operation_specific_errors() { + for (packages, build_error, validation_error) in [ + ( + Vec::::new(), + "project build packages must contain between 1 and 8 items", + "project validation packages must contain between 1 and 8 items", + ), + ( + vec!["bad\npackage".into()], + "invalid project build package scope", + "invalid project validation package scope", + ), + ] { + let scope = ProjectOperationScope { packages }; + assert_eq!( + build_request(scope.clone()).validate().unwrap_err(), + build_error + ); + assert_eq!( + validation_request(scope).validate().unwrap_err(), + validation_error + ); + } +} diff --git a/crates/webcodex-core/src/project_validation.rs b/crates/webcodex-core/src/project_validation.rs index c36b09776..0ab7ac055 100644 --- a/crates/webcodex-core/src/project_validation.rs +++ b/crates/webcodex-core/src/project_validation.rs @@ -1,6 +1,10 @@ //! Declarative project validation planning and admission fence. No model argv. +use crate::project_operation::ProjectOperationScopeError; use serde::{Deserialize, Serialize}; +/// Portable project-operation scope retained under the project_validate API name. +pub use crate::project_operation::ProjectOperationScope as ProjectValidationScope; + #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)] #[serde(rename_all = "snake_case")] pub enum ProjectValidationAction { @@ -27,29 +31,6 @@ pub enum ProjectValidationAdapter { Rust, Go, } -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)] -#[serde(deny_unknown_fields)] -pub struct ProjectValidationScope { - /// Portable bounded package scope. Rust adapters interpret values as Cargo - /// package names; Go adapters interpret them as project-relative package patterns. - #[schemars(length(min = 1, max = 8))] - #[schemars(inner(length(min = 1, max = 256)))] - pub packages: Vec, -} - -impl ProjectValidationScope { - fn validate(&self) -> Result<(), String> { - if self.packages.is_empty() || self.packages.len() > 8 { - return Err("project validation packages must contain between 1 and 8 items".into()); - } - if self.packages.iter().any(|package| { - package.is_empty() || package.len() > 256 || package.chars().any(char::is_control) - }) { - return Err("invalid project validation package scope".into()); - } - Ok(()) - } -} /// Test-only selection and evidence policy. Filtering changes the execution /// target; count requirements are postconditions, not executable arguments. @@ -115,7 +96,14 @@ impl ProjectValidationRequest { } validate_relative(self.cwd.as_deref().unwrap_or("."))?; if let Some(scope) = &self.scope { - scope.validate()?; + scope.validate().map_err(|error| match error { + ProjectOperationScopeError::PackageCount => { + "project validation packages must contain between 1 and 8 items".to_string() + } + ProjectOperationScopeError::InvalidPackage => { + "invalid project validation package scope".to_string() + } + })?; } if let Some(test) = &self.test { if self.action != ProjectValidationAction::Test {