Skip to content

Dependency-graph tooling over-counts: a declared edge is not what a consumer actually resolves #281

Description

@thedavidmeister

Future investigation, surfaced by the deploy/library repo split (rainlanguage/rain.factory#46, #280). Not urgent — the current behaviour errs the safe way; this is about cutting provable false positives, not loosening caution.

Observation

Soldeer resolution is flat by default ([soldeer] recursive_deps = false): a repo declares its complete dependency list explicitly, and installing it pulls only the declared package, none of its transitive deps. Verified empirically — a consumer depending on rain-factory 0.1.5 (which declares five deps) with recursive_deps = false gets exactly rain-factory-0.1.5/ in dependencies/ and nothing behind it.

Separately, a package can expose a self-contained surface — e.g. a deploy repo's pin constants (address/bytes32/creation-code constants under src/generated/<tag>/, re-exported by a Lib…Deploy). Those files import nothing, so a consumer that imports only them compiles only them and inherits none of the package's own dependencies. The build touches the constants; the concrete contract, scripts, and their deps never enter it.

The gap

Any tooling that reasons over declared foundry.toml [dependencies] treats "declares a dependency on" as "transitively stands on the whole closure of." That conflation is wrong for a self-contained / pins-only edge.

Concrete manifestation: rain-org-health's dependency graph (roh-scan/src/graph.rs). Its edges are the flat declared deps (correct — mirrors soldeer). But blockers() walks that graph transitively for audit clearance — "a cleared dependency standing on an unaudited one of its own is still sand." So a repo that pin-depends on a deploy repo is modelled as standing on that deploy repo's entire dep closure (openzeppelin, rain-sol-codegen, …), when its build compiles only the pin constants and inherits none of it. The audit graph over-counts the "sand."

The deploy/library split makes this common: post-split, consumers hang off deploy repos purely for self-contained pin constants, and deploy repos carry the richer dependency list — so the over-count grows.

Why it's not just a bug to swat

Over-caution is the correct direction for an audit tool to err: a false "check this" wastes review; a false "all clear" ships unaudited code you don't know about. So the current behaviour is fail-safe and should stay fail-safe. The point is only that the pins-only subset of these false positives is provably safe to drop — self-contained constants cannot transmit a dependency's risk — so cutting them is a precise refinement, not a relaxation.

Direction to investigate

Distinguish a pin-edge / constants-only edge from a behaviour edge by what actually crosses it — import-level analysis (which files/symbols the consumer imports from the dependency), not just [dependencies] parsing. An edge that resolves only to constant declarations (no functions, no inherited contracts, no onward imports) does not transmit the dependency's transitive closure and shouldn't propagate transitive audit-sand.

Open questions:

  • Where the analysis lives — rainix-static already parses manifests + soldeer state, so it's a candidate home for an import-graph pass that both the audit graph and general dep hygiene could consume.
  • Whether to classify at the package level (a package advertises a "pins-only" surface) or per-edge (analyse each consumer's actual imports). Per-edge is precise but heavier.
  • Interaction with recursive_deps = true consumers (they do pull the closure to disk, but still only compile what they import — so the compile-graph guarantee holds regardless).

Context

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Labels

No labels
No labels

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions