Skip to content

[Toolchain][Introspection] Expose a stable package resolve graph and build-unit plan #19

Description

@a19q3

Summary

CellScript has rich compiled-program metadata, but it does not expose a stable machine-readable package resolve graph or the actual build-unit plan.

Today:

  • cellc metadata compiles an entry and emits CompileMetadata for the program/artifact;
  • cellc info serializes the declared manifest;
  • cellc lock reports only node and environment counts;
  • Cell.lock stores resolved source nodes, but consumers must reverse-engineer selection, scopes, feature roots, environment decisions, workspace roots, and build units.

These are different questions. IDEs, CI policy, security review, cache diagnostics, workspace scheduling, update planning, and external builders need a read-only canonical answer to: what will this command resolve and build?

Code evidence

  • metadata is compile metadata, not package metadata:

    CellScript/src/cli/commands.rs

    Lines 2024 to 2054 in 8ae6dc4

    fn metadata(args: MetadataArgs) -> Result<()> {
    let input_path = args.input.unwrap_or_else(|| PathBuf::from("."));
    let input = Utf8Path::from_path(&input_path)
    .ok_or_else(|| crate::error::CompileError::without_span(format!("path '{}' is not valid UTF-8", input_path.display())))?;
    let options = CompileOptions {
    edition: crate::CURRENT_EDITION,
    opt_level: 0,
    output: None,
    debug: false,
    target: args.target,
    target_profile: args.target_profile,
    primitive_compat: None,
    };
    let result = match compile_path(input, options.clone()) {
    Ok(result) => result,
    Err(error) => return Err(diagnostics_to_error(&compile_failure_diagnostics(input, options, error))),
    };
    let json = serde_json::to_string_pretty(&result.metadata)
    .map_err(|error| crate::error::CompileError::without_span(format!("failed to serialize metadata: {}", 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, json)?;
    println!("{}", "Metadata generated".green());
    println!(" Output: {}", output_path.display());
    } else {
    println!("{}", json);
    }
    Ok(())
  • lock JSON reports counts, not the graph:

    CellScript/src/cli/commands.rs

    Lines 4461 to 4477 in 8ae6dc4

    fn lock(args: PackageLockArgs) -> Result<()> {
    refresh_lockfile_from_manifest(std::path::Path::new("."))?;
    let lockfile = Lockfile::read_from_root(std::path::Path::new("."))?.expect("lockfile was just written");
    CommandOutcome {
    machine: serde_json::json!({
    "status": "ok",
    "lockfile": "Cell.lock",
    "schema": lockfile.schema,
    "dependency_nodes": lockfile.dependencies.len(),
    "environments": lockfile.environments.keys().collect::<Vec<_>>(),
    }),
    human_lines: vec![
    "Dependency graph locked".green().to_string(),
    format!(" {} dependency node(s)", lockfile.dependencies.len()),
    ],
    }
    .emit(args.json)
  • info reports declarations, not resolved nodes:

    CellScript/src/cli/commands.rs

    Lines 4480 to 4512 in 8ae6dc4

    fn info(args: InfoArgs) -> Result<()> {
    let pm = PackageManager::new(".");
    let manifest = pm.read_manifest()?;
    let mut human_lines = vec![
    "Package Info:".bold().to_string(),
    format!(" Name: {}", manifest.package.name),
    format!(" Version: {}", manifest.package.version),
    format!(" Description: {}", manifest.package.description),
    format!(" License: {}", manifest.package.license),
    format!(" Authors: {}", manifest.package.authors.join(", ")),
    format!(" Entry: {}", manifest.package.entry),
    " Dependencies:".to_string(),
    ];
    human_lines.extend(manifest.dependencies.iter().map(|(name, dep)| format!(" - {}: {:?}", name, dep)));
    CommandOutcome {
    machine: serde_json::json!({
    "status": "ok",
    "manifest": "Cell.toml",
    "package": manifest.package,
    "dependencies": manifest.dependencies,
    "dev_dependencies": manifest.dev_dependencies,
    "features": manifest.features,
    "environments": manifest.environments,
    "dependency_overrides": manifest.dependency_overrides,
    "resolvers": manifest.resolvers,
    "build": manifest.build,
    "policy": manifest.policy,
    "deploy": manifest.deploy,
    "metadata": manifest.metadata,
    }),
    human_lines,
    }
    .emit(args.json)
  • the incremental cache is one whole-entry content key; its unit reasons are not exposed: https://github.com/CellScript-Labs/CellScript/blob/8ae6dc4a2749fa107ff42c166772600648c19bb7/src/lib.rs#L7079-L7103

Reference architecture

Cargo exposes a versioned metadata schema containing packages, workspace members, the resolved graph, feature activation, dependency kinds/targets, and output roots:

https://github.com/rust-lang/cargo/blob/75d17360928f57ff2a7d2f2da1c753f5fe1926d1/src/ops/cargo_metadata.rs#L15-L100

Cargo separately models and can serialize compilation units keyed by package, target, profile, platform, mode, and features:

https://github.com/rust-lang/cargo/blob/75d17360928f57ff2a7d2f2da1c753f5fe1926d1/src/compiler/unit_graph.rs#L13-L150

Move package-alt separates a rooted PackageGraph from BuildPlan compilation:

https://github.com/MystenLabs/sui/blob/5a9f37431c473fa2f6d49abecbcc6a6d7190f533/external-crates/move/crates/move-package-alt/src/graph/mod.rs#L40-L89

https://github.com/MystenLabs/sui/blob/5a9f37431c473fa2f6d49abecbcc6a6d7190f533/external-crates/move/crates/move-package-alt-compilation/src/build_plan.rs

CellScript should expose its own smaller, CKB-aware model.

Proposed schemas

Resolve graph

A versioned read-only schema should include:

  • root package or workspace member set;
  • canonical package node IDs;
  • package name, namespace, version, edition, compiler requirement;
  • exact source kind and immutable source identity;
  • source and manifest digests;
  • incoming/outgoing aliases and edge kinds;
  • runtime/test/build-reserved scope;
  • requested and effective feature roots;
  • selected CKB environment identity and dependency-local mapping;
  • lock status and whether each value came from manifest, lock, or explicit CLI selection;
  • unreachable/stale nodes and policy warnings.

Build-unit plan

A separate versioned schema should include:

  • selected entry/action/lock;
  • package node;
  • target and artifact format;
  • target profile, VM/ABI/codec identities, compatibility profile;
  • optimization/debug/primitive policy;
  • direct unit dependencies;
  • expected outputs and sidecars;
  • cache key/fingerprint inputs;
  • cached/up-to-date/stale status with a stable reason;
  • production gate/policy requirements.

The graph must be deterministic and path-normalized, but local paths need not be hidden when the user explicitly requests local diagnostic output.

Command contract

Do not overload the existing compiled-program metadata schema. Add a distinct command family or explicit mode.

Read-only graph inspection must:

  • never update Cell.lock;
  • never invoke an external resolver in locked/frozen mode;
  • clearly mark when an unlocked query would need network or mutable resolution;
  • support JSON schema version negotiation;
  • produce stable opaque node IDs;
  • filter by package, environment, features, scope, target/profile, and entry without changing their meaning.

Dependencies and sequencing

The schema can be designed in parallel, but it must not stabilize the current inconsistent resolver behavior as a public API. Production implementation depends on #17 for chain-identity-safe environment selection and on #15/#16/#18 for canonical workspace, package-instance, and compiler-requirement semantics. The first stable fixture must be generated from those corrected semantics, not from a compatibility snapshot of today's member loop.

Acceptance criteria

  • Versioned resolve-graph and build-plan schemas are accepted.
  • Existing cellc metadata retains its compiled-program meaning.
  • Locked/frozen/offline graph queries are side-effect free and deterministic.
  • Workspace, environment, feature, runtime/test, target/profile, and entry selection are represented.
  • Every dependency edge retains alias and selection provenance.
  • Build units expose cache status and a stable rebuild reason.
  • JSON schemas have fixtures and backward-compatibility tests.
  • LSP/VS Code, CI, package audit, and future workspace scheduler consume the same schema.
  • A graph query over the checked-in package_graph example reproduces Cell.lock exactly.
  • A build-plan query followed by build uses the same selected roots and identity fields.

Non-goals

  • exposing internal Rust structs as an accidental public API;
  • replacing typed semantic, ProofPlan, or verified artifact metadata;
  • making an inspection command perform an update;
  • promising Cargo-compatible JSON.

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