Repository navigation
[Toolchain][Compatibility][P1] Enforce package-declared CellScript compiler requirements #18
Description
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:
Lines 40 to 72 in 8ae6dc4
#[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:
Lines 1052 to 1111 in 8ae6dc4
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:
Lines 2282 to 2304 in 8ae6dc4
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:
Cargo's update reporting also distinguishes newer versions that remain compatible with the required Rust version:
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
- The public manifest spelling and SemVer meaning are documented.
- Empty, malformed, and impossible requirements fail with stable diagnostics.
- Root and transitive packages are checked before source loading.
- Registry selection filters incompatible package versions deterministically.
- Locked/frozen builds revalidate the requirement without re-resolving.
- Machine JSON names every incompatible package and incoming edge.
- update can recommend/select a compatible version without silently crossing interface policy.
- Tests cover root, path, Git, Registry, transitive, pre-release, and old-lock cases.
- Edition and exact reproducible compiler identity remain separate fields.
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.
Activity
a19q3 commentedon Sep 11, 2026
Completed on 0.30 in
cd5b1882: package-declared CellScript compiler requirements are enforced during resolution before source loading/compilation, including incompatible and malformed requirement failures. The product capability ledger recordsremaining_work=[]atcdc5b53c. No TAC or release workflow was invoked.