Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions crates/webcodex-core/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -51,3 +52,6 @@ mod project_validation_tests;

#[cfg(test)]
mod project_build_tests;

#[cfg(test)]
mod project_operation_tests;
37 changes: 12 additions & 25 deletions crates/webcodex-core/src/project_build.rs
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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<String>,
}

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 {
Expand All @@ -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(())
}
Expand Down
38 changes: 38 additions & 0 deletions crates/webcodex-core/src/project_operation.rs
Original file line number Diff line number Diff line change
@@ -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<String>,
}

#[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(())
}
}
71 changes: 71 additions & 0 deletions crates/webcodex-core/src/project_operation_tests.rs
Original file line number Diff line number Diff line change
@@ -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::<String>::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
);
}
}
36 changes: 12 additions & 24 deletions crates/webcodex-core/src/project_validation.rs
Original file line number Diff line number Diff line change
@@ -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 {
Expand All @@ -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<String>,
}

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.
Expand Down Expand Up @@ -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 {
Expand Down
Loading