Skip to content

[Toolchain][Upgrade] Add a transactional package, interface, and deployment upgrade plan #20

Description

@a19q3

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:

    CellScript/src/cli/commands.rs

    Lines 4439 to 4458 in 8ae6dc4

    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:

    CellScript/src/package/mod.rs

    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);
  • interface-diff is a strong pairwise check but is not connected to package update or reverse dependency impact:

    CellScript/src/cli/commands.rs

    Lines 2093 to 2121 in 8ae6dc4

    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

  • A dry-run plan is the default or is available before any lock mutation.
  • Candidate resolution is isolated and deterministic.
  • Package-scoped/precise updates do not unexpectedly repin unrelated nodes.
  • Dependency build/interface identities needed for impact analysis are preserved or recomputed with evidence.
  • Reverse dependents are compiled and classified before apply.
  • Six interface dimensions remain separate in the report.
  • Upgrade authorization is separate from compatibility.
  • Apply verifies old-lock and plan hashes, then writes atomically.
  • Frozen/offline plan verification never accesses the network.
  • Negative tests cover downgrade, source substitution, stale plan, interface break, layout break, builder break, profile change, and unauthorized TYPE_ID lineage.
  • The workflow integrates with the future graph schema and ProtocolBundle without treating either as consensus authorization.

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions