Skip to content

scripts: add split_rust_module.py for mechanical, verifiable Rust module splits - #562

Merged
ac-pplx merged 15 commits into
mainfrom
ac/split-rust-module-tool
Oct 16, 2026
Merged

ac-pplx merged 15 commits into
mainfrom
ac/split-rust-module-tool

Conversation

@ac-pplx

@ac-pplx ac-pplx commented Oct 16, 2026 •

Copy link
Copy Markdown
Contributor

What

scripts/split_rust_module.py splits a rustfmt-formatted Rust file into child modules from a short spec, and proves afterwards that the result is a pure move.

  • list FILE prints each top-level item, and each member of an inherent impl, with the selector a spec uses.
  • apply SPEC moves the selected items into new child modules. Each item keeps its doc comments and attributes, its original order, and its text. The parent keeps mod child; and a glob re-export, so every existing path still resolves and no caller changes. Each child starts with use super::*;, so the parent's imports, traits and macros resolve as before.
  • verify [BASE] proves that BASE...HEAD's Rust changes are exactly what apply generates. It derives the spec from the diff (which items each new child module holds), regenerates the split from the merge base, rustfmts the result, and requires HEAD's parent and child files to equal it byte for byte. Any other Rust change fails it: a missing or altered mod/use line, a dropped or reordered item, an edit inside moved code or a comment, a symlink, or a diff with no split. So does a non-Rust change inside the split crate (a manifest, data or build input) unless --allow-non-rust is given; non-Rust changes elsewhere are listed. --print-spec shows the derived spec so it can be compared with the one in the PR.

A spec names items, not lines:

file = "crates/libs/x/src/foo.rs"
[[module]]
name = "pull"
doc = "Pull jobs: destination locks, cancellation cleanup and the pull itself."
items = ["struct DestinationPullGuard", "impl Drop for DestinationPullGuard",
         "impl TransferExecutor :: fn execute_pull*"]

impl <header> :: fn <name> moves chosen members of an inherent impl into a block with the same attributes and header in the child; the parent's block keeps the rest. Trait impls move whole.

Why

Large files are slow to review and change, for people and for agents. Splitting them by hand takes long and is hard to review. A split also goes stale against main within hours, and rebasing a 1 → N move is mostly manual, because git's rename detection doesn't follow it.

With this tool a split is a spec plus a regenerate. If main moves, rerun apply on the new main instead of resolving conflicts. Reviewers check the grouping in the spec and rerun verify, instead of reading thousands of moved lines.

verify checks the generator's output, not its semantics. Those rest on the edits below being reach-preserving, on the refusals, and on compiling and testing the result like any change.

The only edits it makes to moved text

Each keeps name resolution and reach exactly as they were:

Edit Why
private / pub(self) → pub(super) on items, inherent-impl members and struct fields One module deeper, pub(super) reaches exactly the old module and its descendants.
pub(super) → pub(in crate::<grandparent>) (or pub(crate)) Same reach as before.
super:: → super::super:: The code is one module deeper.
relative include!/include_str!/include_bytes! paths gain ../ Only when the children are one directory below the parent file (foo.rs → foo/).

It refuses, rather than guesses, when a move needs a hand edit. Most refusals are about macros, which the tool cannot expand:

  • moving use, mod x;, extern blocks or macro_rules! items, and any split of a parent with a #[macro_use] item. Macros stay in the parent, above the new mod lines, so children still see them.
  • moved code reaching a crate macro whose expansion names super/self paths or emits pub(super)/pub(self). This covers direct calls, calls through other macros, path-qualified calls and use .. as aliases.
  • a macro invoked before the parent defines it, or defined twice;
  • super passed in a user macro's arguments, bare or path-qualified. format!/assert!/tracing-style macros are fine, unless the crate defines its own macro of the same name;
  • stringify! of super, and include!;
  • a doc comment or #[doc] attribute in moved code naming a super::/self:: path: an intra-doc link would point one module off;
  • top-level or impl-member macro invocations, unless the module sets allow_macro_items;
  • relative visibility the edits cannot remap: pub(in super::..), a pub(super) nested inside a moved item, an inline module naming super, self::super, or a super:: path inside a string literal;
  • include arguments other than a plain string literal;
  • a parent file loaded through #[path], or #[path] inside moved code;
  • a module name that is a keyword, collides with an item or import of the parent, or is already used as a path in the parent or its child modules;
  • an attribute on the same line as its item or impl member;
  • a carriage return, a src/bin/<name>.rs crate root, or a code block in a module doc.

Module options, each reproduced by verify:

  • allow_unused_glob keeps use super::*; under #[allow(unused_imports)] for a module that needs nothing from its parent.
  • allow_unused_reexport does the same for the parent's re-export when nothing outside the module names its items.
  • allow_macro_items opts in to moving macro invocations.

apply also reports per new module the tracing events without an explicit target: (their target becomes the new module path), sql! locators, and lines matching an archcheck pattern_ratchet, so a PR can list them and move ratchet entries with the code.

Evidence

  • scripts/tests/test_split_rust_module.py (108 tests):
    • the visibility table, file layouts and re-export reach;
    • every refusal above, and every change verify must reject;
    • a compile oracle: rustc --test -D warnings builds and runs a small crate before and after a split, with callers in the parent, a sibling and the grandparent reaching private, pub(super) and field-level items.
  • Workspace compile run: all items of 19 large production files in 15 crates moved at once, 1,838 items in total.
    • cargo check --workspace --all-targets --all-features and CI's clippy invocations with -D warnings passed.
    • The --no-default-features builds of the 14 packages, doctests, and the nextest runs of four packages all matched main.
  • Round trip: on the 102 largest production files, every item and impl member was moved, rustfmt'd and checked with verify. 92 regenerate identically; the other 10 stop at a refusal (macro invocations without the opt-in, include!, a parent macro emitting pub(super), nested relative visibility, an inline module naming super).
  • Eight split PRs (node-ctl: split facets.rs into facet and local-state modules (pure move) #563–api: move six groups out of controllers/substrate/mod.rs (pure move) #569, spaced-host-client: move the client's methods into modules by guest area (pure move) #571) use it. Each regenerates byte-identically with this version.
  • Review: four rounds by Codex and Claude. Rounds 1–3 found false accepts in an earlier verify design, which led to the regeneration check, and apply cases that changed meaning; each is fixed with a test. Round 4 found only macro forms the tool now refuses. A maintainer review then found seven non-blocking gaps; each is fixed with a test, and the eight split PRs still regenerate identically.

Limits

  • It expects rustfmt layout: top-level items in column 0.
  • Splitting a single large function is not a move, so it is out of scope.
  • verify proves the commit is apply's rustfmt'd output for the derived spec. That apply preserves meaning rests on the edit rules, the refusals, and compiling and testing the result. Macros are where that argument is weakest, hence the conservative refusals.
  • Macros defined outside the crate being split are not analyzed: other workspace crates, dependencies, proc and attribute macros. One that emits pub(super) or a super:: path would expand one module deeper without a refusal; compiling and testing the result is the check.
  • What a module moved with allow_macro_items emits keeps the visibility the macro gives it; compile errors are the check.
  • Log targets and source locations of moved code change. That is inherent to moving code; docs/rust/space-conventions.md ("Log targets") covers it, and apply counts them.

🤖 Generated with Claude Code

Splitting a large Rust file into child modules by hand is slow to review
and painful to rebase. This tool does it from a short spec that names the
items each new child module gets:

- `list FILE` prints every top-level item with the selector a spec uses.
- `apply SPEC` moves the named items verbatim, with their doc comments and
  attributes and in their original order, into new child modules. The
  parent keeps `mod child;` and a glob re-export, so every existing path
  still resolves and no caller changes.
- `verify [BASE]` proves BASE...HEAD is such a move. It re-applies the
  same edits to every item at the merge base and requires an exact match
  at HEAD, ignoring whitespace and trailing commas but not string literals.

The only edits to moved text are the ones that keep name resolution and
reach exactly as they were:

- Visibility on items, inherent-impl members and struct fields: private
  becomes `pub(super)` and `pub(super)` becomes `pub(in <grandparent>)`,
  which is the same reach one module deeper.
- `super::` paths gain one `super::`.
- Relative `include_str!`/`include_bytes!` paths gain `../` when the
  children sit one directory below the parent file.

Each child starts with `use super::*;`, so the parent's imports, traits
and macros resolve as before. Because the spec names items rather than
lines, a split can be regenerated on a newer main instead of being
rebased.
A large file is often one big inherent impl, so whole-item moves cannot
shrink it. A selector of the form 'impl <header> :: fn <name>' now moves
chosen members into an impl block with the same attributes and header in
the child module. The parent's block keeps the rest and disappears once
it is empty. The impl's doc comments stay with the parent's block, or go
to the first child that receives members when none are left. Trait impls
still move whole.

verify compares inherent impls member by member, keyed by the impl's
attributes and header, so a split impl is proved the same way as a whole
item.

apply now also refuses a parent file loaded through #[path], whose
children would resolve elsewhere, and reports archcheck pattern-ratchet
matches each new module would hold, so their baseline entries can move
with the code.
Moving every item of 19 large files into child modules compiled and
passed clippy, doctests and the affected crates' tests apart from
these cases, now handled:

- `const _` takes no visibility; giving it pub(super) is a warning.
- A macro_rules! that emits pub(super) or pub(self) means something
  else when it is expanded one module deeper, so apply refuses to move
  code that invokes it.
- verify ignores the trailing comma rustfmt adds inside generics when
  a visibility pushes a signature past the line width.
- The #[path] check ignores attributes written in comments.
File headers (//! docs and #![...] inner attributes) are not items, so
verify did not compare them, and an added #![cfg(...)] or #![allow(...)]
would have passed. A modified file's header must now equal its base,
and a new child module's header may hold only //! docs.
…arden apply

Review found that verify's item matching accepted changes that are not
moves: a dropped mod or re-export line, an import added next to the
generated ones, a header edit, an edit a whitespace-insensitive
comparison erases ((x,) to (x), a newline inside a string), doc comments
swapped between impls, and items reordered in the parent.

verify now proves something simpler and stronger: it derives the spec
from the diff (which items each new child module holds), regenerates
the split from the merge base with apply, and compares every parent and
child file byte for byte after rustfmt. Any other Rust change, a
symlink, or a diff with no split in it fails.

apply fixes from the same review and from a rustc-built test crate:

- Pick the glob re-export's visibility from each item's reach, so
  pub(in crate::a) items are no longer re-exported more narrowly, and
  re-export modules of macro invocations at pub, which a glob clamps.
- Count angle brackets only directly inside a struct body and skip
  const-generic blocks when finding a body or impl brace, so fields
  after [u8; 1 << 3] and impls with where T: Tr<{ 1 + 1 }> are widened.
- Match extern "C" members, treat src/bin/*.rs as crate roots and a
  nested main.rs as an ordinary module, keep cfg text byte for byte on
  the new mod lines, rewrite super:: in attributes copied onto split
  impl blocks, and rebase plain include! paths.
- Refuse instead of guessing: a macro name defined twice, macro chains
  that emit pub(super), nested relative visibility, inline modules that
  name super, self::super, super:: inside string literals, other
  include arguments, keyword or import-colliding module names, and an
  attribute sharing a line with its item.
- glob_import = false omits use super::*; for a module that needs
  nothing from its parent. The reexport option is gone, so a dropped
  re-export cannot pass verify.
- The #[path] scan stays inside the crate.

Tests: 63, including rejection cases for each false accept above and a
compile oracle that builds and runs a small crate with rustc before and
after a split, with callers at every visibility boundary.
verify:
- Format only the regenerated side and compare it with HEAD's bytes.
  Comparing two rustfmt outputs let an edit rustfmt erases (a trailing
  comma inside a macro call) pass.
- Find a new file's parent from the same layout rules apply uses, so a
  split of a src/bin/ crate root round-trips.
- The glob import is never optional: a module whose items need nothing
  from the parent sets allow_unused_glob, which keeps `use super::*;`
  under #[allow(unused_imports)]. Removing the import, which can change
  what a name resolves to, now fails verify.

apply now refuses, rather than changing meaning:
- moved code that invokes a parent macro whose expansion names a
  super/self path, directly or through another macro;
- moved code that invokes a macro the parent defines only later in the
  file (it bound to an outer macro of that name);
- top-level macro invocations, unless the module sets
  allow_macro_items (their items keep visibilities the tool cannot
  remap);
- inline modules nested inside moved code that names super;
- stringify! of a super path with any delimiter;
- an include inside a split impl's header or attributes;
- an attribute sharing a line with an impl member.

apply also deepens every path-leading super (use super as x, not only
super::), rebases include paths written with {} or [] delimiters,
wraps generated //! docs at 100 columns, and counts the two kinds of
visibility edit separately.
ac-pplx added a commit that referenced this pull request Oct 16, 2026
…ure move)

sandbox/worker.rs is keyed by emitter: every sandbox-manager feature
appends its metric family here. Move each family into its own child
module: guest_bridge, route_authorization, net_gateway,
teardown_suspend, auto_lifecycle, cleanup, quarantine_recovery,
inventory, health and ingress. New families then get a module of their
own instead of growing this file.

Generated by scripts/split_rust_module.py (#562) from the spec in the
pull request. Items move verbatim. The only edits keep each item's
reach: 10 private items, fields or methods become pub(super). The parent
re-exports the moved items, so no caller changes.
verify:
- Read file bytes without newline translation, and reject a carriage
  return. rustc ends a // comment only at \n, so a lone CR after a
  comment turns the next line into comment text that Python's newline
  translation would hide.
- Map every impl, not only inherent ones, back to its base selector,
  and derive a whole-impl selector when one child holds all of an
  impl's members. A whole move of an impl with super:: in its header or
  #[doc = include_str!] on it no longer fails as a false reject.
- Write the derived spec without ASCII escaping, so non-ASCII docs
  round-trip.
- Resolve the repository root, so verify works from a subdirectory.
- List the non-Rust files a change also touches, which verify does not
  check.

apply:
- Refuse include! of Rust code, whose paths resolve from the module it
  is expanded in. include_str!/include_bytes! paths are still rebased.
- Refuse a file with a carriage return.
- Refuse a src/bin/<name>.rs crate root: its children would sit in
  src/bin/, where Cargo builds every file as its own binary.
- Refuse a module name the parent already uses as a path (a crate or a
  glob-imported module), which the new module would shadow.
- Ignore #[...] inside block comments when reading an item's attributes,
  and refuse a multi-line attribute that ends on its item's line.
- Leave const _ out of the re-export choice, so a module of only
  impls and const _ gets no unused glob re-export.

Remove dead ranking code. Tests: 82, with message checks on the
rejection tests and cases for each finding above.
ac-pplx added a commit that referenced this pull request Oct 16, 2026
facets.rs holds the facet registry and dispatch together with every
facet's implementation. Keep the registry, dispatch and shared publish
types in facets.rs and move each facet into its own child module:
network_policy, template_desired_state, sandbox_desired_state,
cluster_deleted_eviction, port_exposure, inventory_publish and the
local_state reads they share.

Generated by scripts/split_rust_module.py (#562) from the spec in the
pull request. Items move verbatim. The only edits keep each item's
reach: 101 private items, fields or methods become pub(super). The
parent re-exports the moved items, so no caller changes.
ac-pplx added a commit that referenced this pull request Oct 16, 2026
…ure move)

sandbox/worker.rs is keyed by emitter: every sandbox-manager feature
appends its metric family here. Move each family into its own child
module: guest_bridge, route_authorization, net_gateway,
teardown_suspend, auto_lifecycle, cleanup, quarantine_recovery,
inventory, health and ingress. New families then get a module of their
own instead of growing this file.

Generated by scripts/split_rust_module.py (#562) from the spec in the
pull request. Items move verbatim. The only edits keep each item's
reach: 10 private items, fields or methods become pub(super). The parent
re-exports the moved items, so no caller changes.
ac-pplx added a commit that referenced this pull request Oct 16, 2026
data_plane_sessions.rs serves five unrelated ClusterApi RPC families:
session route resolution, process binding and reconnects, revocation,
runtime access, and the ECW sandbox service route with its reset and
credential checks. Move each family, and the route-row and route-token
helpers they share, into its own child module: session_route,
route_rows, route_token, service_route, service_route_reset,
ecw_authorization, process_binding, revocation and runtime_access.

Generated by scripts/split_rust_module.py (#562) from the spec in the
pull request. Items move verbatim. The only edits keep each item's
reach: 132 private items, fields or methods become pub(super); 8
pub(super) ones become pub(crate). The parent re-exports the moved
items, so no caller changes.
ac-pplx added a commit that referenced this pull request Oct 16, 2026
facets.rs holds the facet registry and dispatch together with every
facet's implementation. Keep the registry, dispatch and shared publish
types in facets.rs and move each facet into its own child module:
network_policy, template_desired_state, sandbox_desired_state,
cluster_deleted_eviction, port_exposure, inventory_publish and the
local_state reads they share.

Generated by scripts/split_rust_module.py (#562) from the spec in the
pull request. Items move verbatim. The only edits keep each item's
reach: 101 private items, fields or methods become pub(super). The
parent re-exports the moved items, so no caller changes.
ac-pplx added a commit that referenced this pull request Oct 16, 2026
bootstrap.rs collects five unrelated facilities that services use at
startup: gRPC request parameter recording and logging, Unix socket binding, the
HTTP trace layer, the gRPC trace layer and shutdown signal handling.
Keep the gRPC server builder and health service in bootstrap.rs and
move each facility into its own child module: grpc_request_params,
unix_socket, http_trace, grpc_trace and shutdown.

Generated by scripts/split_rust_module.py (#562) from the spec in the
pull request. Items move verbatim. The only edits keep each item's
reach: 42 private items, fields or methods become pub(super). The parent
re-exports the moved items, so no caller changes.
ac-pplx added a commit that referenced this pull request Oct 16, 2026
executor.rs is the largest production file in the workspace. Besides
the transfer executor it holds the page-cache writeback coordinator,
the decode and decrypt sinks, sparse-shadow comparison, replica
provenance, cgroup codec sizing and the pull path. Move those, which no
open pull request edits, into child modules: codec_concurrency,
artifact_role, transfer_metrics, sparse_shadow, replica_provenance,
stream_pull_timing, writeback_coordinator, stream_writeback,
decode_sinks and pull. Payload, crypto staging, the artifact job and
push stay for a later step.

Generated by scripts/split_rust_module.py (#562) from the spec in the
pull request. Items move verbatim. The only edits keep each item's
reach: 207 private items, fields or methods become pub(super). The
parent re-exports the moved items, so no caller changes.
ac-pplx added a commit that referenced this pull request Oct 16, 2026
node_maintenance.rs serves three unrelated admin route families beside
the Direct node registry: node status, health and reconcile; node
template pull, delete and list through the node-api; and the node-api
client plumbing they share. Keep the registry handlers and the shared
node row and response types in node_maintenance.rs and move the rest
into child modules: node_api, node_status and node_templates.

Every line that touches a Direct-only table stays in
node_maintenance.rs, so archcheck's direct_tables ratchet is unchanged.

Generated by scripts/split_rust_module.py (#562) from the spec in the
pull request. Items move verbatim. The only edits keep each item's
reach: 137 private items, fields or methods become pub(super). The
parent re-exports the moved items, so no caller changes.
ac-pplx added a commit that referenced this pull request Oct 16, 2026
substrate/mod.rs holds the SubstrateAdapter trait and its dispatch, and
also a dozen unrelated groups of types and helpers. Move six of them,
which no open pull request edits, into child modules: node_api_client,
pool_claims, local_artifact, execution_fence, recovery_charge and
sandbox_state_probe. The trait, dispatch, errors, capabilities and
request types stay for a later step.

Generated by scripts/split_rust_module.py (#562) from the spec in the
pull request. Items move verbatim. The only edits keep each item's
reach: 20 private items, fields or methods become pub(super); 2
pub(super) ones become pub(in crate::controllers). The parent re-exports
the moved items, so no caller changes.
- Check macro textual scope transitively: a moved item that reaches a
  parent macro through another parent macro (relay!() expanding to
  value!()) is refused when that macro is defined later in the parent
  or more than once, like a direct invocation.
- Detect a terminal super (use super as x) in parent macro bodies, as in
  moved code.
- Read the attributes copied onto split impl blocks from masked text, so
  a #[cfg(..)] inside a block comment is not copied as a real one.
- Refuse a multi-line impl-member attribute that ends on its member's
  line, which would otherwise attach the member to the next one.

Tests: 86.
ac-pplx added a commit that referenced this pull request Oct 16, 2026
node_maintenance.rs serves three admin route families beside the Direct
node registry: node status, health and reconcile; node template pull and
delete through the node-api, and listing from the cached template
inventory; and the node-api client plumbing the registry and template
routes use. Keep the registry handlers, the shared node row and response
types, and the node reads in node_maintenance.rs, and move the rest into
child modules: node_api, node_status and node_templates.

Every line that touches a Direct-only table stays in
node_maintenance.rs, so archcheck's direct_tables ratchet is unchanged.

Generated by scripts/split_rust_module.py (#562) from the spec in the
pull request. Items move verbatim. The only edits keep each item's
reach: 137 private items, fields or methods become pub(super). The
parent re-exports the moved items, so no caller changes.
- Deepen super passed as a macro argument (call!(super)); the
  visibility check already excludes pub(super).
- Refuse a raw-identifier inline module (mod r#i { .. }) naming super,
  like any inline module.
- Treat #[macro_use] items like macro_rules! when placing the new mod
  lines, so children still see the macros they bring in.
- Require allow_macro_items to move a macro invocation that is an
  inherent-impl member, as for top-level invocations.
- Scan macro_rules! definitions elsewhere in the crate (and use-aliases
  of them) for super/self paths, not only the parent's.
- Look for a new module name used as a path in the parent's existing
  child modules too, so it cannot shadow a crate they name.
- Refuse a code block in a module doc, which rustdoc would run.
- Remove dead code.

Tests: 93.
A module whose moved items are named only inside the module (and its
tests) makes the parent's private use child::*; unused in normal builds,
which -D warnings rejects. Dropping the re-export is not safe, because a
sibling's glob import could then resolve the name to a different item.
allow_unused_reexport = true keeps the re-export under
#[allow(unused_imports)]; verify derives it from the head like the other
module options.
ac-pplx added a commit that referenced this pull request Oct 16, 2026
executor.rs is the largest production file in the workspace. Besides the
transfer executor it holds the page-cache writeback coordinator, the
decode and decrypt sinks, sparse-shadow comparison, replica provenance,
cgroup codec sizing and the pull path. Move those, which no open pull
request edits, into child modules: codec_concurrency, artifact_role,
transfer_metrics, sparse_shadow, replica_provenance, stream_pull_timing,
writeback_coordinator, stream_writeback, decode_sinks and pull. Payload,
crypto staging, the artifact job and push stay for a later step.

Generated by scripts/split_rust_module.py (#562) from the spec in the
pull request. Items move verbatim. The only edits keep each item's
reach: 210 private items, fields or methods become pub(super). The
parent re-exports the moved items, so no caller changes.
ac-pplx added a commit that referenced this pull request Oct 16, 2026
…rea (pure move)

lib.rs holds the whole typed client: its types, the call macro and
constructors, and 40 methods across four guest services. Keep the types,
the macro and the constructors in lib.rs and move the methods into impl
blocks in five child modules: meta, filesystem, file_watch, process and
networking, together with the helpers only those methods use.

Generated by scripts/split_rust_module.py (#562) from the spec in the
pull request. Items move verbatim. The only edits keep each item's
reach: 5 private items, fields or methods become pub(super). The parent
re-exports the moved items, so no caller changes.
ac-pplx added a commit that referenced this pull request Oct 16, 2026
data_plane_sessions.rs serves five unrelated ClusterApi RPC families:
session route resolution, process binding and reconnects, revocation,
runtime access, and the ECW sandbox service route with its reset and
credential checks. Move each family, and the route-row and route-token
helpers they share, into its own child module: session_route,
route_rows, route_token, service_route, service_route_reset,
ecw_authorization, process_binding, revocation and runtime_access.

Generated by scripts/split_rust_module.py (#562) from the spec in the
pull request. Items move verbatim. The only edits keep each item's
reach: 132 private items, fields or methods become pub(super); 8
pub(super) ones become pub(crate). The parent re-exports the moved
items, so no caller changes.
The fourth review round found the remaining meaning changes all come
from macros, which the tool cannot expand. Refuse them instead:

- Treat a macro_rules! anywhere in the crate as relative when its body
  names a super/self path or relative visibility, or reaches such a
  macro through other macros or a use-alias, and refuse moved code that
  invokes one, bare or path-qualified.
- Refuse passing super in a user macro's arguments: the macro can match
  or stringify those tokens, so deepening them may change its expansion.
  format-, assert- and tracing-style macros stay allowed.
- Refuse any split of a parent with a #[macro_use] item, whose textual
  scope the new mod lines would change.
- Scan the parent's child modules recursively when checking that a new
  module name is not already used as a path.

The workspace has no #[macro_use] and no super passed to a user macro,
so no real split loses anything. Docstring updated; tests: 98.
ac-pplx added a commit that referenced this pull request Oct 16, 2026
substrate/mod.rs holds the SubstrateAdapter trait and its dispatch, and
also a dozen unrelated groups of types and helpers. Move six of them,
which no open pull request edits, into child modules: node_api_client,
pool_claims, local_artifact, execution_fence, recovery_charge and
sandbox_state_probe. The trait, dispatch, errors, capabilities and
request types stay for a later step.

Generated by scripts/split_rust_module.py (#562) from the spec in the
pull request. Items move verbatim. The only edits keep each item's
reach: 20 private items, fields or methods become pub(super); 2
pub(super) ones become pub(in crate::controllers). The parent re-exports
the moved items, so no caller changes.
@ac-pplx
ac-pplx marked this pull request as ready for review October 16, 2026 14:49
@the-gigi-pplx

Copy link
Copy Markdown
Collaborator

Review: 7 findings

Medium

  1. scripts/split_rust_module.py:636: the refusal for super passed in a user macro's arguments can be bypassed two ways. The module docstring says this case is refused.

    • Path-qualified calls skip it. The pattern is (?<![\w:])(\w+)\s*!, so crate::m!(..), $crate::m!(..) and other_crate::m!(..) are never checked. Reproduced on a scratch crate. With #[macro_export] macro_rules! name_of { ($p:path) => { stringify!($p) } } in lib.rs, a moved crate::name_of!(super::Thing) was rewritten to crate::name_of!(super::super::Thing) and apply exited 0. The bare call name_of!(super::Thing) was refused, as intended. The result still compiles, but the string it produces changes.
    • A crate macro that shares a name with an entry in KNOWN_MACROS (line 557) is exempt by name alone. Reproduced: a local macro_rules! ensure { ($c:expr) => { if !$c { panic!("{}", stringify!($c)) } } }. A moved ensure!(super::LIMIT > 0) became ensure!(super::super::LIMIT > 0) with no refusal, which changes the panic text.

    verify can't catch either one, because the output is exactly what apply generates. To fix it, check the last path segment of a qualified call too, and apply the KNOWN_MACROS exemption only when the crate's own macro_rules! bodies (all_bodies) don't define that name. None of the split PRs that depend on this one (PR node-ctl: split facets.rs into facet and local-state modules (pure move) #563 to PR spaced-host-client: move the client's methods into modules by guest area (pure move) #571) rewrites a single super:: path, so none of them is affected.

Low

  1. scripts/split_rust_module.py:238: a file that opens with a plain // comment (a license or SPDX line) and then //! docs isn't parsed as having a header. Both comments attach to the first item and move with it. Reproduced: splitting out fn a put // Copyright header. and //! Module docs. into p/bar.rs after use super::*;, which is E0753, and left the parent with no module docs. The compiler catches it, so it's loud rather than silent, but the header should stay with the parent. /*! .. */ inner docs aren't recognized either.
  2. scripts/split_rust_module.py:653: super:: is rewritten only in masked code, where comments are blanked, so intra-doc links in moved doc comments keep their old text. Reproduced: a moved /// Returns the crate root's [`super::Thing`] stayed as written while the code beside it became super::super::Thing. One module deeper, the link resolves to the parent's Thing. That's a different item when the parent has one, and a broken link when it doesn't. Refusing (or deepening) super::/self:: in doc-link syntax would close it. None of the dependent splits has such a link; I grepped every new child file.
  3. scripts/split_rust_module.py:822: DEFS has no fn, so in a parent made only of functions the anchor falls through to a later mod/use. Reproduced: with fn a, fn b, #[cfg(test)] mod tests;, splitting out fn a writes mod bar; and pub use bar::*; after mod tests; at the bottom of the file. Every regeneration will put them there again.
  4. scripts/split_rust_module.py:1108: the comment says a fully moved split impl's docs go to "the first module (in spec order) that receives members". Lines 1114 and 1115 actually pick the module that receives the lowest-indexed member, which isn't the same when the spec lists modules in a different order from the members. Either the comment or the code should change, so the derived placement is predictable.
  5. scripts/split_rust_module.py:1024: the relative-macro refusal only scans macro_rules! bodies under the parent's own crate. A macro from another workspace crate or a dependency, or a proc or attribute macro, that emits pub(super) or a super:: path expands one module deeper without a refusal. The docstring scopes the check to "any macro_rules! in the crate", so this matches what it promises. It's still worth listing among the things verify doesn't prove, next to the compile-and-test caveat. I read this one and didn't reproduce it.
  6. verify exits 0 when the diff also changes non-Rust files. I appended a line to crates/libs/spaced/host/client/Cargo.toml on top of PR spaced-host-client: move the client's methods into modules by guest area (pure move) #571's head. verify printed not checked by verify (not Rust): …/Cargo.toml and exited 0. A manifest, feature or include_str! data change could then ride along in a "pure move" whose only evidence is the exit code. Exiting non-zero on non-Rust changes under the split crate unless a flag allows them would make the exit code mean what the body says it does.

Verified against dcfd1ae230db3453800e227e522be5b97995bbb8.

What I ran:

  • pytest scripts/tests/test_split_rust_module.py: 98 passed.
  • verify origin/main on all eight dependent split PRs: identical, exit 0 for each.
  • Re-ran apply from each PR body's spec on its merge base: byte-identical to the head every time.
  • Twelve hand mutations on PR spaced-host-client: move the client's methods into modules by guest area (pure move) #571's head: verify caught all eleven Rust ones. They were a comment edit in a moved item, a narrowed visibility, use super::{*};, an added item, a trailing comment, pub mod in the parent, an unrelated parent attribute, a new build.rs, a stray child file, a comment inside a body, and port → port: port. The twelfth, the Cargo.toml edit in finding 7, passed.
  • Findings 1 to 4 were reproduced on a scratch crate and finding 7 on PR spaced-host-client: move the client's methods into modules by guest area (pure move) #571's head. Findings 5 and 6 come from reading the cited lines.

@the-gigi-pplx the-gigi-pplx left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approved. 7 non-blocking findings in the review comment above.

- Refuse super passed to a path-qualified user macro (crate::m!,
  $crate::m!, other::m!), and to a crate macro_rules! that shares a
  name with a known format/assert/tracing/anyhow macro: the exemption
  now applies only when the crate defines no macro of that name.
- Keep plain comments (a license or SPDX line) above the module's
  inner docs in the parent header, and recognize /*! */ inner docs.
- Refuse moved code whose doc comments or #[doc] attributes name a
  super:: or self:: path: an intra-doc link would point one module off.
- Place the new mod lines above the first fn when the parent defines no
  types, instead of after a trailing mod tests;.
- verify fails on non-Rust changes inside the split crate (a manifest,
  data or build input) unless --allow-non-rust is given.
- Document that macros from other crates, and proc and attribute
  macros, are not analyzed; fix the comment on split-impl doc placement.

All eight open split branches still verify identical with this tool.
Tests: 108.
@ac-pplx

ac-pplx commented Oct 16, 2026

Copy link
Copy Markdown
Contributor Author

Thanks for the careful read. All seven are fixed in a097b25, each with a test that fails on the previous head and passes now (108 tests):

  1. super in macro arguments. The scan now matches the last path segment, so crate::m!, $crate::m! and other::m! are checked. The KNOWN_MACROS exemption applies only when no macro_rules! in the crate has that name; apply collects the crate's macro names before the transforms run.
  2. Header. The header runs through the last //! line, /*! */ block or #![..] attribute before the first item, including the plain comments and blank lines above it. A license or SPDX line above the module docs stays with the parent.
  3. Intra-doc links. apply refuses moved code whose doc comments (///, //!, /** */, /*! */) or #[doc] attributes name a super:: or self:: path. Plain comments and code are not affected; a test covers * super::V on a continuation line.
  4. Declaration placement. With no type, const or impl in the parent, the mod lines go above the first fn or inline mod, not after a trailing mod tests;. fn is still not in DEFS, so parents that have definitions place them as before.
  5. Lead-owner comment. It now says what the code does: the module that receives the impl's lowest-indexed moved member.
  6. Macros from other crates. The docstring and the Limits section of the description now say that macros from other workspace crates, dependencies, and proc or attribute macros are not analyzed, and that compiling and testing is the check there.
  7. Non-Rust changes. verify now fails on a non-Rust change inside the split crate (a manifest, data or build input) unless --allow-non-rust is given. Changes elsewhere are still listed.

The split PRs are unaffected: with this version, all eight (#563–#569, #571) still regenerate byte-identically at their current heads, none of their parents starts with a plain comment, and every non-Rust file they touch is outside the split crate.

@ac-pplx
ac-pplx enabled auto-merge October 16, 2026 16:27
@ac-pplx
ac-pplx added this pull request to the merge queue Oct 16, 2026
Merged via the queue into main with commit 562ef46 Oct 16, 2026
20 checks passed
@ac-pplx
ac-pplx deleted the ac/split-rust-module-tool branch October 16, 2026 16:49
kavishkartha05 pushed a commit to kavishkartha05/SPACE that referenced this pull request Oct 16, 2026
…erplexityai#569)

* api: move six groups out of controllers/substrate/mod.rs (pure move)

substrate/mod.rs holds the SubstrateAdapter trait and its dispatch, and
also a dozen unrelated groups of types and helpers. Move six of them,
which no open pull request edits, into child modules: node_api_client,
pool_claims, local_artifact, execution_fence, recovery_charge and
sandbox_state_probe. The trait, dispatch, errors, capabilities and
request types stay for a later step.

Generated by scripts/split_rust_module.py (perplexityai#562) from the spec in the
pull request. Items move verbatim. The only edits keep each item's
reach: 20 private items, fields or methods become pub(super); 2
pub(super) ones become pub(in crate::controllers). The parent re-exports
the moved items, so no caller changes.

* docs: point references at the split mod.rs modules
kavishkartha05 pushed a commit to kavishkartha05/SPACE that referenced this pull request Oct 16, 2026
…rea (pure move) (perplexityai#571)

lib.rs holds the whole typed client: its types, the call macro and
constructors, and 40 methods across four guest services. Keep the types,
the macro and the constructors in lib.rs and move the methods into impl
blocks in five child modules: meta, filesystem, file_watch, process and
networking, together with the helpers only those methods use.

Generated by scripts/split_rust_module.py (perplexityai#562) from the spec in the
pull request. Items move verbatim. The only edits keep each item's
reach: 5 private items, fields or methods become pub(super). The parent
re-exports the moved items, so no caller changes.
rranjan14 pushed a commit to rranjan14/SPACE that referenced this pull request Oct 23, 2026
…ve) (perplexityai#563)

facets.rs holds the facet registry and dispatch together with every
facet's implementation. Keep the registry, dispatch and shared publish
types in facets.rs and move each facet into its own child module:
network_policy, template_desired_state, sandbox_desired_state,
cluster_deleted_eviction, port_exposure, inventory_publish and the
local_state reads they share.

Generated by scripts/split_rust_module.py (perplexityai#562) from the spec in the
pull request. Items move verbatim. The only edits keep each item's
reach: 101 private items, fields or methods become pub(super). The
parent re-exports the moved items, so no caller changes.
rranjan14 pushed a commit to rranjan14/SPACE that referenced this pull request Oct 23, 2026
)

executor.rs is the largest production file in the workspace. Besides the
transfer executor it holds the page-cache writeback coordinator, the
decode and decrypt sinks, sparse-shadow comparison, replica provenance,
cgroup codec sizing and the pull path. Move those, which no open pull
request edits, into child modules: codec_concurrency, artifact_role,
transfer_metrics, sparse_shadow, replica_provenance, stream_pull_timing,
writeback_coordinator, stream_writeback, decode_sinks and pull. Payload,
crypto staging, the artifact job and push stay for a later step.

Generated by scripts/split_rust_module.py (perplexityai#562) from the spec in the
pull request. Items move verbatim. The only edits keep each item's
reach: 210 private items, fields or methods become pub(super). The
parent re-exports the moved items, so no caller changes.
rranjan14 pushed a commit to rranjan14/SPACE that referenced this pull request Oct 23, 2026
…ure move) (perplexityai#565)

sandbox/worker.rs is keyed by emitter: every sandbox-manager feature
appends its metric family here. Move each family into its own child
module: guest_bridge, route_authorization, net_gateway,
teardown_suspend, auto_lifecycle, cleanup, quarantine_recovery,
inventory, health and ingress. New families then get a module of their
own instead of growing this file.

Generated by scripts/split_rust_module.py (perplexityai#562) from the spec in the
pull request. Items move verbatim. The only edits keep each item's
reach: 10 private items, fields or methods become pub(super). The parent
re-exports the moved items, so no caller changes.
rranjan14 pushed a commit to rranjan14/SPACE that referenced this pull request Oct 23, 2026
…erplexityai#566)

* internal-api: split data_plane_sessions.rs by RPC family (pure move)

data_plane_sessions.rs serves five unrelated ClusterApi RPC families:
session route resolution, process binding and reconnects, revocation,
runtime access, and the ECW sandbox service route with its reset and
credential checks. Move each family, and the route-row and route-token
helpers they share, into its own child module: session_route,
route_rows, route_token, service_route, service_route_reset,
ecw_authorization, process_binding, revocation and runtime_access.

Generated by scripts/split_rust_module.py (perplexityai#562) from the spec in the
pull request. Items move verbatim. The only edits keep each item's
reach: 132 private items, fields or methods become pub(super); 8
pub(super) ones become pub(crate). The parent re-exports the moved
items, so no caller changes.

* archcheck: re-key the direct_tables entry for the split data_plane_sessions.rs

The split moves all seven direct_tables matches in
data_plane_sessions.rs into data_plane_sessions/route_rows.rs. Re-key
the ratchet entry to the new file with the same cap of 7.

* docs: point references at the split data_plane_sessions.rs modules

node_api_backend_endpoint now lives in data_plane_sessions/route_rows.rs;
repoint the network policy comment that cites it.
rranjan14 pushed a commit to rranjan14/SPACE that referenced this pull request Oct 23, 2026
)

* common-svc: split bootstrap.rs by facility (pure move)

bootstrap.rs collects five unrelated facilities that services use at
startup: gRPC request parameter recording and logging, Unix socket binding, the
HTTP trace layer, the gRPC trace layer and shutdown signal handling.
Keep the gRPC server builder and health service in bootstrap.rs and
move each facility into its own child module: grpc_request_params,
unix_socket, http_trace, grpc_trace and shutdown.

Generated by scripts/split_rust_module.py (perplexityai#562) from the spec in the
pull request. Items move verbatim. The only edits keep each item's
reach: 42 private items, fields or methods become pub(super). The parent
re-exports the moved items, so no caller changes.

* docs: point references at the split bootstrap.rs modules

Two chart comments cite bootstrap.rs for replacing a node-local socket
by rename. That code now lives in bootstrap/unix_socket.rs.
rranjan14 pushed a commit to rranjan14/SPACE that referenced this pull request Oct 23, 2026
…plexityai#568)

* api: split admin node_maintenance.rs by route family (pure move)

node_maintenance.rs serves three admin route families beside the Direct
node registry: node status, health and reconcile; node template pull and
delete through the node-api, and listing from the cached template
inventory; and the node-api client plumbing the registry and template
routes use. Keep the registry handlers, the shared node row and response
types, and the node reads in node_maintenance.rs, and move the rest into
child modules: node_api, node_status and node_templates.

Every line that touches a Direct-only table stays in
node_maintenance.rs, so archcheck's direct_tables ratchet is unchanged.

Generated by scripts/split_rust_module.py (perplexityai#562) from the spec in the
pull request. Items move verbatim. The only edits keep each item's
reach: 137 private items, fields or methods become pub(super). The
parent re-exports the moved items, so no caller changes.

* docs: point references at the split node_maintenance.rs modules

The Kind node-api chart comment cites pull_node_template by file. It now
lives in node_maintenance/node_templates.rs; require_admin_node_registry
stays in node_maintenance.rs.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants