Summary
CellScript has strong individual pieces for upgrade safety, but no graph-wide transactional upgrade workflow.
Existing pieces include:
- exact Cell.lock source graph;
- six-dimensional public interface compatibility;
- package Registry admission checks;
- generated builder and deployment identity;
- Deployed.toml verification and TYPE_ID lineage consistency.
However, cellc update currently resolves and writes Cell.lock, then prints the selected nodes. It does not first present a candidate diff, preserve dependency build/interface identities, compile affected reverse dependents, classify interface changes, regenerate builders, or evaluate deployment policy.
Code evidence
- update mutates the lock before reporting results:
|
fn update() -> Result<()> { |
|
refresh_lockfile_from_manifest(std::path::Path::new("."))?; |
|
let lockfile = Lockfile::read_from_root(std::path::Path::new("."))?.expect("lockfile was just written"); |
|
if lockfile.dependencies.is_empty() { |
|
println!("{}", "No dependencies to update".green()); |
|
} else { |
|
println!("{}", format!("Updated {} dependency nodes", lockfile.dependencies.len()).green()); |
|
for (node_id, package) in &lockfile.dependencies { |
|
let source = match &package.source { |
|
crate::package::LockedSource::Path { path } => format!("path: {}", path), |
|
crate::package::LockedSource::Git { url, revision } => format!("git: {}#{}", url, revision), |
|
crate::package::LockedSource::Registry { registry, namespace, version, .. } => { |
|
format!("registry: {}/{}@{}", registry, namespace, version) |
|
} |
|
}; |
|
println!(" {} {} v{} ({})", node_id, package.name, package.version, source); |
|
} |
|
} |
|
|
|
Ok(()) |
- normal resolution writes every dependency node with build: None, discarding the optional LockedBuildInfo channel:
|
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); |
- interface-diff is a strong pairwise check but is not connected to package update or reverse dependency impact:
|
fn interface_diff(args: InterfaceDiffArgs) -> Result<()> { |
|
let old = read_or_compile_interface(&args.old)?; |
|
let new = read_or_compile_interface(&args.new)?; |
|
let report = crate::interface::compare(&old, &new); |
|
let json = serde_json::to_string_pretty(&report) |
|
.map_err(|error| crate::error::CompileError::without_span(format!("failed to serialize interface report: {error}")))?; |
|
if let Some(output_path) = args.output.as_ref() { |
|
if let Some(parent) = output_path.parent() { |
|
std::fs::create_dir_all(parent)?; |
|
} |
|
std::fs::write(output_path, format!("{json}\n"))?; |
|
} |
|
if args.json || args.output.is_none() { |
|
println!("{json}"); |
|
} else { |
|
println!("Interface compatibility: {}", if report.compatible { "compatible" } else { "breaking" }); |
|
for dimension in &report.dimensions { |
|
println!(" {}: {}", dimension.dimension, dimension.classification); |
|
} |
|
if let Some(output_path) = args.output.as_ref() { |
|
println!(" Output: {}", output_path.display()); |
|
} |
|
} |
|
if !report.compatible { |
|
return Err(crate::error::CompileError::without_span("public interface contains breaking changes").with_code("E2501")); |
|
} |
|
Ok(()) |
|
} |
|
|
- deployment verification checks recorded identities, but it is not an authorization policy or pre-update impact plan.
Why this matters
A source-compatible package update can still change:
- serialized layouts;
- runtime or witness ABI;
- effects/capabilities;
- generated builder requirements;
- deployment/code-cell identity;
- target/profile/codec/checker schemas;
- transitive dependency identities;
- CKB cycles, occupied capacity, and transaction shape.
Conversely, a deployment upgrade can be authorized but interface-incompatible. Authorization and compatibility are independent.
Move/Sui and Cargo lessons
Sui makes upgrade authorization explicit through UpgradeCap/UpgradeTicket/UpgradeReceipt, binds a digest, and prevents policy widening:
https://github.com/MystenLabs/sui/blob/5a9f37431c473fa2f6d49abecbcc6a6d7190f533/crates/sui-framework/packages/sui-framework/sources/package.move#L251-L315
Sui execution separately applies compatible, additive, or dependency-only bytecode checks:
https://github.com/MystenLabs/sui/blob/5a9f37431c473fa2f6d49abecbcc6a6d7190f533/sui-execution/latest/sui-adapter/src/static_programmable_transactions/execution/context.rs#L2489-L2563
Cargo provides precise package selection and compiler-version-aware update guidance, but does not supply CellScript's on-chain interface/deployment guarantees.
CellScript should combine its own stronger metadata with the separation of resolution, compatibility, and authorization.
Dependencies and sequencing
A transactional upgrade plan must consume the corrected package semantics rather than compensate for them locally: #17 closes environment identity, #15/#16/#18 define workspace/package/compiler selection, and #19 exposes the canonical resolve graph and build-unit plan. #9 and #11 are downstream integration points for ProtocolBundle and interface-handle impact; they are not substitutes for the graph foundation.
Proposed workflow
1. Resolve candidate in memory
Given the current lock and explicit selection:
- compute candidate nodes without modifying Cell.lock;
- record exact old/new source, version, manifest digest, feature, environment, and graph edges;
- support package-scoped and precise selection;
- identify added, removed, upgraded, downgraded, source-switched, and feature/environment-changed nodes.
2. Compute reverse impact
Using the canonical resolve/build graph:
3. Evaluate policy
Classify independently:
- source/package SemVer policy;
- public-interface compatibility;
- serialized state migration requirement;
- runtime ABI and target-profile compatibility;
- builder migration requirement;
- deployment compatibility;
- upgrade authorization;
- release/gate evidence required.
No single compatible boolean should collapse these dimensions.
4. Present and apply transactionally
Emit a canonical plan with stable issue codes and a human summary. Apply only after the selected policy passes and the user explicitly chooses the apply operation. Write Cell.lock atomically. Never deploy, publish, sign, or mutate Deployed.toml as an implicit consequence of package update.
Suggested plan fields
- schema version and plan hash;
- old/new lock hashes;
- requested package/precision/environment/features;
- node and edge diff;
- reverse-dependent build units;
- old/new interface and artifact identity;
- per-dimension compatibility changes;
- migration artifacts required;
- deployment/upgrade authorization status;
- required gates and evidence;
- blocking diagnostics and explicit acknowledgements;
- apply preconditions to prevent a stale plan.
Acceptance criteria
Non-goals
- automatic on-chain deployment;
- automatic state migration;
- treating SemVer as proof of compatibility;
- trusting a mutable Registry lookup during consensus;
- weakening the existing interface-diff or deployment verification boundaries.
Summary
CellScript has strong individual pieces for upgrade safety, but no graph-wide transactional upgrade workflow.
Existing pieces include:
However, cellc update currently resolves and writes Cell.lock, then prints the selected nodes. It does not first present a candidate diff, preserve dependency build/interface identities, compile affected reverse dependents, classify interface changes, regenerate builders, or evaluate deployment policy.
Code evidence
CellScript/src/cli/commands.rs
Lines 4439 to 4458 in 8ae6dc4
CellScript/src/package/mod.rs
Lines 2282 to 2304 in 8ae6dc4
CellScript/src/cli/commands.rs
Lines 2093 to 2121 in 8ae6dc4
Why this matters
A source-compatible package update can still change:
Conversely, a deployment upgrade can be authorized but interface-incompatible. Authorization and compatibility are independent.
Move/Sui and Cargo lessons
Sui makes upgrade authorization explicit through UpgradeCap/UpgradeTicket/UpgradeReceipt, binds a digest, and prevents policy widening:
https://github.com/MystenLabs/sui/blob/5a9f37431c473fa2f6d49abecbcc6a6d7190f533/crates/sui-framework/packages/sui-framework/sources/package.move#L251-L315
Sui execution separately applies compatible, additive, or dependency-only bytecode checks:
https://github.com/MystenLabs/sui/blob/5a9f37431c473fa2f6d49abecbcc6a6d7190f533/sui-execution/latest/sui-adapter/src/static_programmable_transactions/execution/context.rs#L2489-L2563
Cargo provides precise package selection and compiler-version-aware update guidance, but does not supply CellScript's on-chain interface/deployment guarantees.
CellScript should combine its own stronger metadata with the separation of resolution, compatibility, and authorization.
Dependencies and sequencing
A transactional upgrade plan must consume the corrected package semantics rather than compensate for them locally: #17 closes environment identity, #15/#16/#18 define workspace/package/compiler selection, and #19 exposes the canonical resolve graph and build-unit plan. #9 and #11 are downstream integration points for ProtocolBundle and interface-handle impact; they are not substitutes for the graph foundation.
Proposed workflow
1. Resolve candidate in memory
Given the current lock and explicit selection:
2. Compute reverse impact
Using the canonical resolve/build graph:
3. Evaluate policy
Classify independently:
No single compatible boolean should collapse these dimensions.
4. Present and apply transactionally
Emit a canonical plan with stable issue codes and a human summary. Apply only after the selected policy passes and the user explicitly chooses the apply operation. Write Cell.lock atomically. Never deploy, publish, sign, or mutate Deployed.toml as an implicit consequence of package update.
Suggested plan fields
Acceptance criteria
Non-goals