Summary
PackageInfo already contains cellscript_version, but package validation and dependency resolution do not enforce it.
The field is currently inert for source packages. A dependency can therefore declare a compiler/toolchain requirement that the active cellc does not satisfy, and resolution proceeds until parsing, lowering, metadata validation, or artifact generation fails for some unrelated reason.
Edition is not a substitute for a minimum compiler version:
- edition selects language semantics;
- compiler requirement selects the minimum toolchain that understands the manifest, package schema, language surface, target profile, and metadata/checker contracts;
- reproducible artifact identity records the exact compiler and build inputs.
These must remain separate.
Code evidence
- PackageInfo declares cellscript_version:
|
#[derive(Debug, Clone, Serialize, Deserialize)] |
|
pub struct PackageInfo { |
|
pub name: String, |
|
pub version: String, |
|
pub edition: CellScriptEdition, |
|
#[serde(default)] |
|
pub namespace: Option<String>, |
|
#[serde(default)] |
|
pub authors: Vec<String>, |
|
#[serde(default)] |
|
pub description: String, |
|
#[serde(default)] |
|
pub license: String, |
|
#[serde(default)] |
|
pub repository: String, |
|
#[serde(default)] |
|
pub homepage: String, |
|
#[serde(default)] |
|
pub documentation: String, |
|
#[serde(default)] |
|
pub keywords: Vec<String>, |
|
#[serde(default)] |
|
pub categories: Vec<String>, |
|
#[serde(default)] |
|
pub cellscript_version: String, |
|
#[serde(default = "default_entry")] |
|
pub entry: String, |
|
#[serde(default)] |
|
pub source_roots: Vec<String>, |
|
#[serde(default)] |
|
pub include: Vec<String>, |
|
#[serde(default)] |
|
pub exclude: Vec<String>, |
- validate_manifest_package_contract checks package SemVer, environments, resolver declarations, and reserved build dependencies, but never reads cellscript_version:
|
fn validate_manifest_package_contract(&self, manifest: &PackageManifest) -> Result<()> { |
|
semver::Version::parse(&manifest.package.version).map_err(|error| { |
|
CompileError::without_span(format!( |
|
"package '{}' has invalid semantic version '{}': {error}", |
|
manifest.package.name, manifest.package.version |
|
)) |
|
})?; |
|
for (name, environment) in &manifest.environments { |
|
validate_environment(name, environment)?; |
|
} |
|
for environment in manifest.dependency_overrides.keys() { |
|
if !manifest.environments.contains_key(environment) { |
|
return Err(CompileError::without_span(format!( |
|
"dependency override environment '{}' has no matching [environments.{}] declaration", |
|
environment, environment |
|
))); |
|
} |
|
} |
|
for (name, resolver) in &manifest.resolvers { |
|
if name.trim().is_empty() { |
|
return Err(CompileError::without_span("resolver names must not be empty")); |
|
} |
|
let command = Path::new(&resolver.command); |
|
if !command.is_absolute() { |
|
return Err(CompileError::without_span(format!("resolver '{}' command must be an absolute executable path", name))); |
|
} |
|
let digest = resolver.sha256.strip_prefix("sha256:").unwrap_or(&resolver.sha256); |
|
if digest.len() != 64 || !digest.bytes().all(|byte| byte.is_ascii_hexdigit()) { |
|
return Err(CompileError::without_span(format!("resolver '{}' sha256 must contain exactly 32 bytes of hex", name))); |
|
} |
|
} |
|
let declared_dependencies = manifest |
|
.dependencies |
|
.iter() |
|
.chain(manifest.dev_dependencies.iter()) |
|
.chain(manifest.dependency_overrides.values().flat_map(|dependencies| dependencies.iter())); |
|
for (alias, dependency) in declared_dependencies { |
|
if let Dependency::Detailed(detail) = dependency |
|
&& let Some(resolver) = detail.resolver.as_deref() |
|
{ |
|
if detail.path.is_some() || detail.git.is_some() { |
|
return Err(CompileError::without_span(format!( |
|
"dependency '{}' cannot combine resolver with path or git", |
|
alias |
|
))); |
|
} |
|
if !manifest.resolvers.contains_key(resolver) { |
|
return Err(CompileError::without_span(format!( |
|
"dependency '{}' selects undeclared resolver '{}'", |
|
alias, resolver |
|
))); |
|
} |
|
} |
|
} |
|
if !manifest.build.dependencies.is_empty() { |
|
return Err(CompileError::without_span( |
|
"[build.dependencies] is reserved until isolated build-script execution is implemented; use [dependencies] for compile-time imports", |
|
)); |
|
} |
|
Ok(()) |
- dependency lock nodes have an optional build.compiler_version, but normal resolution writes build: None:
|
pub fn update_from_resolved(&mut self, resolved: &BTreeMap<String, ResolvedPackage>) { |
|
for (node_id, package) in resolved { |
|
let locked = LockedDependency { |
|
name: package.name.clone(), |
|
namespace: package.namespace.clone(), |
|
version: package.version.clone(), |
|
source: match &package.source { |
|
PackageSource::Local(path) => LockedSource::Path { path: path.to_string_lossy().to_string() }, |
|
PackageSource::Git { url, revision } => LockedSource::Git { url: url.clone(), revision: revision.clone() }, |
|
PackageSource::Registry { registry, url, revision, namespace, version } => LockedSource::Registry { |
|
registry: registry.clone(), |
|
url: url.clone(), |
|
revision: revision.clone(), |
|
namespace: namespace.clone(), |
|
version: version.clone(), |
|
}, |
|
}, |
|
source_hash: package.source_hash.clone(), |
|
manifest_digest: package.manifest_digest.clone(), |
|
dependencies: package.dependencies.clone(), |
|
build: None, |
|
}; |
|
self.dependencies.insert(node_id.clone(), locked); |
- Registry records carry a cellscript_version string, but that does not make local or locked source compilation enforce it.
Cargo comparison
Cargo treats package.rust-version as an active compatibility constraint. Before compilation it checks every unit against the current rustc and reports all incompatible packages with upgrade or precise-version guidance:
https://github.com/rust-lang/cargo/blob/75d17360928f57ff2a7d2f2da1c753f5fe1926d1/src/ops/cargo_compile/mod.rs#L642-L690
Cargo's update reporting also distinguishes newer versions that remain compatible with the required Rust version:
https://github.com/rust-lang/cargo/blob/75d17360928f57ff2a7d2f2da1c753f5fe1926d1/src/ops/cargo_update.rs#L540-L648
CellScript needs the same principle, adapted to its stronger edition/profile/checker identity boundaries.
Proposed semantics
Define a normative manifest key and remove ambiguity around the existing field. For example, it may mean a SemVer requirement over cellc releases, not an exact artifact identity.
Required behavior:
- parse the requirement with the same standard SemVer implementation used for package versions;
- validate the root package before loading source;
- validate every dependency during candidate selection and again during locked materialization;
- reject candidates whose compiler requirement is incompatible with the active cellc;
- include the requirement in manifest digests and package/Registry metadata;
- preserve the selected package requirement and actual compiler version in machine-readable build evidence;
- make cellc update capable of selecting the newest package version compatible with the active compiler;
- provide an explicit, auditable override only if a concrete migration workflow requires it; do not silently ignore the field.
An exact compiler build/source hash still belongs in reproducible build evidence. Do not force exact-compiler pinning merely to express source compatibility.
Compatibility dimensions
The check should report separately:
- compiler release range;
- CellScript edition;
- manifest/lock schema version;
- target and target-profile support;
- metadata and independent-checker schema compatibility;
- optional feature requirements.
This avoids the misleading conclusion that matching one version string proves artifact reproducibility.
Acceptance criteria
Non-goals
- claiming semantic equivalence between compiler releases;
- weakening exact compiler identity in release/reproducibility evidence;
- treating a Registry record as proof that an artifact was built by the declared compiler.
Summary
PackageInfo already contains cellscript_version, but package validation and dependency resolution do not enforce it.
The field is currently inert for source packages. A dependency can therefore declare a compiler/toolchain requirement that the active cellc does not satisfy, and resolution proceeds until parsing, lowering, metadata validation, or artifact generation fails for some unrelated reason.
Edition is not a substitute for a minimum compiler version:
These must remain separate.
Code evidence
CellScript/src/package/mod.rs
Lines 40 to 72 in 8ae6dc4
CellScript/src/package/mod.rs
Lines 1052 to 1111 in 8ae6dc4
CellScript/src/package/mod.rs
Lines 2282 to 2304 in 8ae6dc4
Cargo comparison
Cargo treats package.rust-version as an active compatibility constraint. Before compilation it checks every unit against the current rustc and reports all incompatible packages with upgrade or precise-version guidance:
https://github.com/rust-lang/cargo/blob/75d17360928f57ff2a7d2f2da1c753f5fe1926d1/src/ops/cargo_compile/mod.rs#L642-L690
Cargo's update reporting also distinguishes newer versions that remain compatible with the required Rust version:
https://github.com/rust-lang/cargo/blob/75d17360928f57ff2a7d2f2da1c753f5fe1926d1/src/ops/cargo_update.rs#L540-L648
CellScript needs the same principle, adapted to its stronger edition/profile/checker identity boundaries.
Proposed semantics
Define a normative manifest key and remove ambiguity around the existing field. For example, it may mean a SemVer requirement over cellc releases, not an exact artifact identity.
Required behavior:
An exact compiler build/source hash still belongs in reproducible build evidence. Do not force exact-compiler pinning merely to express source compatibility.
Compatibility dimensions
The check should report separately:
This avoids the misleading conclusion that matching one version string proves artifact reproducibility.
Acceptance criteria
Non-goals