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
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 onrain-factory 0.1.5(which declares five deps) withrecursive_deps = falsegets exactlyrain-factory-0.1.5/independencies/and nothing behind it.Separately, a package can expose a self-contained surface — e.g. a deploy repo's pin constants (
address/bytes32/creation-codeconstants undersrc/generated/<tag>/, re-exported by aLib…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). Butblockers()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 toconstantdeclarations (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:
rainix-staticalready 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.recursive_deps = trueconsumers (they do pull the closure to disk, but still only compile what they import — so the compile-graph guarantee holds regardless).Context
recursive_depsbehaviour + the pins-are-self-contained finding: this conversation's split ofrain.factory.rainlanguage/rain-org-healthplugins/rain-org-health-check/roh-scan/src/graph.rs(foundry_dependencies,graph_edges,blockers,deps_beneath).