This guide defines how modules and items should be named in this repo. The goal is a clear public API, not a mechanical rewrite rule.
- Choose the public path first, then choose the source name.
- Use modules to express domain boundaries and meaningful facets.
- Do not force everything into exactly one module level.
- Keep public item names short when the surrounding module path already provides the missing context.
- Keep internal names more descriptive when local clarity benefits from it, then re-export a shorter public name if needed.
- Prefer import styles that leave the namespace visible at call sites.
- Use
lower_snake_case. - Name modules for stable concepts, domains, or facets.
- Add a module level only when that segment adds a real axis of meaning.
- Good module roles include
error,email,http,policy,query,command,store,repo, and similar domain terms. - Avoid catch-all names like
common,misc,helpers, ortypesunless the boundary is genuinely shared and a more specific name would be misleading. - Stop nesting when the extra segment only repeats the parent or mirrors file layout without improving the API.
- Use
PascalCasefor types and traits. - Use
snake_casefor functions and methods. - Use
SCREAMING_SNAKE_CASEfor constants and statics. - Inside a descriptive module, prefer short leaf names such as
user::Repository,user::Id,user::Error,user::Request. - Generic leaf names such as
Repository,Error,Id, andRequestonly make sense when the parent path already tells the reader which domain they belong to. - If the parent path is weak or purely technical, keep the entity name in the leaf or move the item under a stronger module.
- Do not repeat the module name in the public leaf unless that extra prefix carries meaning that the path does not already provide.
- Prefer
user::Repositoryoveruser::UserRepositorywhen theusermodule already establishes the domain.
- Prefer
user::Repositoryoveruser::UserRepository. The module already carries theusercontext. - Prefer
storage::UserRepositoryoverstorage::Repository.storageis a technical boundary, not the domain itself. - Prefer
user::error::InvalidEmailoveruser::InvalidEmailError. The nested path makes the category explicit without stuffing it into the leaf. - Prefer
user::email::Addressoveruser::EmailAddresswhenemailis a real subdomain with more than one item. Ifemailis only a single isolated concept,user::EmailAddressmay still be the better shape.
One module level is not a hard rule. Use as many levels as the API needs, as long as each level is doing real work.
Examples:
user::RepositoryTheusermodule already provides the domain, so the leaf can stay generic.user::error::InvalidEmailUse this shape whenerroris a useful category and the leaf is a concrete variant or subtype within that category.user::email::ErrorUse this shape whenemailis the real subdomain andErroris that subdomain's main error type.user::http::RequestUse this shape when transport or protocol is the extra axis that matters.
It is fine to expose both a broad domain item and a deeper specialized item:
user::Erroruser::error::InvalidEmail
That pattern works when user::Error is the umbrella domain error and user::error::* holds narrower classified types.
The rule is not "always shorten the leaf." The rule is "shorten the leaf when the path still reads clearly."
The public API does not have to match the internal source name exactly.
Use pub use ... as ... when:
- a longer source name improves local readability or searchability
- generated code or macro output is clearer with a prefixed name
- the public path is cleaner with a shorter leaf name
Example:
// src/user/mod.rs
pub mod error;
mod repository;
pub use error::UserError as Error;
pub use repository::UserRepository as Repository;That lets internal code keep UserError and UserRepository while the public API stays:
user::Erroruser::Repository
That still leaves room for deeper items such as user::error::InvalidEmail when the nested module carries real meaning.
Use aliases to simplify the API, not to invent synonyms that hide real differences.
Prefer imports that preserve the module boundary:
use crate::domain::user;
fn load(repo: &user::Repository) {}Prefer that over flattening the leaf import when the module name carries useful meaning:
use crate::domain::user::Repository;Direct leaf imports are still acceptable for traits, macros, tests, or tight local scopes where repeated qualification is just noise. They should be the exception when they erase a useful namespace.
Apply a net-context test before preserving a qualifier. If the leaf already says the generic category clearly and the qualifier mostly repeats it, the qualified form is noise rather than clarity. Paths like response::Response or error::Error should usually be shortened at call sites.
When adding or renaming an item, apply these checks:
- Is the parent module descriptive enough that the leaf can be short?
- If not, should the item keep a longer leaf name instead of becoming too generic?
- Does another module segment add a real domain or facet boundary?
- Does the public leaf repeat information that is already present in the path?
- Would an internal long name plus a public re-export produce a cleaner API?
- Will the import style preserve the shape of the namespace at call sites?
Codex should default to these behaviors:
- Do not mechanically turn every prefixed name into exactly one
head::Tailsplit. - Consider whether
user::auth::Token,user::error::InvalidEmail, oruser::http::Requestis the better public path. - Prefer the path that reads best at call sites.
- Treat lint suggestions as candidates, not commands; if qualification adds no net meaning, prefer the shorter call-site shape and fix the lint.
- Only shorten a leaf to
Repository,Error,Id, or similar when the parent path already supplies the missing context. - If a longer source name is still useful, keep it internally and re-export the shorter public name.
- If a lint implies a module refactor, do the filesystem refactor for real. Don't mount old files under prettier module names with
#[path = ...]shims.
modum enforces the subset of this guide that can be checked reliably across a workspace:
namespace_flat_useWarning. Flags flattened imports of generic nouns such asRepositoryorError, and also family leaves when parsed source shows a clear local witness that the namespace still matters at call sites, such asviewer::ResolutionFlowalongsideviewer::ResolutionStateandviewer::ResolutionOutcome. It skips cases where the only preserved form would still be redundant, such aserror::Errororresponse::Response.namespace_flat_use_preserve_moduleWarning. Flags flattened imports from configured namespace-preserving modules such asemail,http,query, orstoragewhen the preserved call-site form still adds net context.namespace_flat_use_redundant_leaf_contextWarning. Flags flattened imports or actionable rename-heavy aliases such asuse user::UserRepository;oruse playwright::api::page::Event as PageEvent;where the child module already supplies the missing context. For plain imports, this only applies when the shorter leaf would land on an actionable generic noun such asRepository,Error, orId. For rename aliases, this only applies when the preserved qualifier still adds real context at call sites.namespace_family_unsupported_constructAdvisory warning. Flags imports, re-exports, or type aliases where namespace-family call-site inference was skipped because the parsed source for the sibling family includes unsupported observation gaps such as#[cfg],macro_rules!, other item macros, orinclude!.modumemits this instead of pretending the family witness is complete.namespace_redundant_qualified_genericWarning. Flags qualified call-site paths such asresponse::Responseorerror::Errorwhen the qualifier only repeats a generic category that the leaf already names clearly. When the written path resolves through an imported namespace and the redundant canonical path could read better from a nearer parent surface, it can recommend that promotable parent surface for owned code even if that export does not exist yet. It does not invent new parent surfaces for external crates such asstd,core,serde, oraxum.namespace_aliased_qualified_pathWarning. Flags qualified call-site paths such aswrite_back_domain::Submissionwhen a renamed snake_case namespace alias has flattened a semantic path likedomain::write_backinto a technical local synonym. If the resolved canonical path is itself redundant, it suggests the final readable surface instead, including a promotable nearer parent surface such asinbound::SourceUpdatewhen that is the nicer API for owned code even though the export does not exist yet. It does not invent new parent surfaces for external crates.namespace_parent_surfaceWarning. Flags imports that reach into an internal child module even though the containing parent surface already exposes the same readable item, such as preferringhttp::Errorovererror::Error.namespace_flat_pub_useWarning. Flags flattened public re-exports of those generic nouns when they bypass a meaningful child facet. It does not flag module-root re-exports that intentionally create the canonical parent surface, such aspub use error::Error;foruser::Error.namespace_flat_pub_use_preserve_moduleWarning. Flags flattened public re-exports from configured namespace-preserving modules when the child facet should stay visible. It does not flag broader parent-surface exports such as exposing bothcomponents::Buttonandpartials::Button, and it skips private alias modules that only exist to feed a parent surface.- Semantic child module namespaces such as
tab_set::ContentPropsare already carrying context at call sites. Do not force a broader parent likecomponents::tab_set::ContentPropsunless that broader parent adds net meaning. namespace_flat_pub_use_redundant_leaf_contextWarning. Flags flattened public re-exports whose leaf still repeats the parent context. It does not flag family re-exports from private organizational child modules when flattening them is how the parent surface is intentionally shaped.api_missing_parent_surface_exportWarning. Flags public child modules that expose a same-name main item such asbutton::Button, or a sole generic main item such astoxicity::Outcome, without also re-exporting the readable parent surface. It does not treat the crate root as automatically readable enough for domain items likedomain::User.api_weak_module_generic_leafWarning. Flags paths such asstorage::Repositorywhere a weak technical module exposes a generic leaf.api_redundant_leaf_contextWarning. Flags paths such asuser::UserRepositoryorpage::ErrorPagewhere the leaf repeats the parent context, and also flags flat public leaves such asUserRepositorywhen a sibling semantic module surface likeuser::Repositoryalready exists.api_candidate_semantic_moduleAdvisory warning. Flags sibling public items such asUserRepository,UserService, andUserIdwhen at least three shared-head items suggest a semantic module surface likeuser::{Repository, Service, Id}. It also flags select 2-member high-signal families such asTransactionIdplusTransactionFailureorHttpRequestplusHttpResponsewhen both shortened leaves are single-segment boundary nouns. When those siblings also share a common tail, such asCaseInboxLaunch,CaseDetailLaunch, andCaseAuditLaunch, it can suggest a nested surface likecase::launch::{Inbox, Detail, Audit}. It also flags families such asCompletedOutcome,RejectedOutcome, andtoxicity::Outcomewhen their shared generic tail suggests a semantic module surface likeoutcome::{Completed, Rejected, Toxicity}. It skips weak promoted heads liketo,has,open, androlled, and it skips hidden or internal module scopes. This is a parsed-source heuristic, not a macro-expanded or cfg-pruned authority surface.internal_path_shim_moduleWarning. Flags internal#[path = ...] mod ...;shims when the attribute is being used to mount code under a different semantic module shape, such as#[path = "../create_room_flow.rs"] mod create;insideflow::room. If the structure wantsflow::room::create, create the real module files or directories and move the code there.api_path_shim_moduleWarning. Flags surface-visible#[path = ...] mod ...;shims when they keep an older filesystem name under a nicer caller-facing module name. If the public path wantsrequest_meta, make that a real module path instead of leaving the old file behind a shim.api_candidate_semantic_module_unsupported_constructAdvisory warning. Flags scopes where semantic-module family inference was skipped because the parsed source includes unsupported observation gaps such as#[cfg],macro_rules!, other item macros, orinclude!.modumemits this instead ofapi_candidate_semantic_modulewhen the raw source is too weak to justify that heuristic.api_manual_enum_string_helperAdvisory warning. Flags public enum string surfaces that are spelled manually, including non-const methods likelabel()oras_str(), free helpers likescenario_label(&Scenario), and manualDisplayimpls that only map variants to string literals.api_ad_hoc_parse_helperAdvisory warning. Flags public enum parse helpers such as freeparse_*functions and inherent methods likeMode::parse(&str) -> Result<Self, _>whenFromStrorTryFrom<&str>would be a more standard boundary surface. It skips helpers when the enum already implementsFromStrin the same scope.api_parallel_enum_metadata_helperAdvisory warning. Flags public enums that expose several parallel metadata helpers such aslabel(),code(), andsource_term()over repeatedmatch selfblocks, when a typed descriptor surface would model that metadata more cleanly.api_strum_serialize_all_candidateWarning. Flags per-variantstrumstring attributes that could be replaced by one enum-levelserialize_allrule without changing the external strings.api_builder_candidateWarning. Flags public constructors or workflow entrypoints that take several positional weak parameters and would likely read better as a builder or typed options struct. It skips functions already marked with a builder surface.api_repeated_parameter_clusterWarning. Flags repeated public constructor or workflow signatures that reuse the same ordered named parameter cluster across entrypoints, when a shared options type orbonbuilder would likely avoid duplicating the call shape.api_optional_parameter_builderWarning. Flags builder-shaped public entrypoints that take positionalOption<_>parameters and would likely read better as abonbuilder, so callers can omit unset values instead of passingNone.api_defaulted_optional_parameterWarning. Flags builder-shaped public entrypoints that immediately default positionalOption<_>parameters, when abonbuilder would let callers omit those values entirely.api_standalone_builder_surfaceAdvisory warning. Flags publicwith_*orset_*free-function families that collectively behave like a builder surface for one type.api_boolean_protocol_decisionWarning. Flags publicboolparameters or fields that encode a domain or protocol decision rather than a runtime toggle.api_forwarding_compat_wrapperWarning. Flags explicit conversion helpers such asto_*orinto_*methods that only forward to an existingFromconversion already present in the crate.api_stringly_protocol_collectionAdvisory warning. Flags public const or static collections that enumerate protocol, state, transition, artifact, gate, or step values as raw strings instead of typed enums or descriptor maps.api_stringly_model_scaffoldAdvisory warning. Flags public structs that carry several semantic descriptor fields as raw strings, such asstate_path,kind_label, andnext_machine, when those concepts would likely read better as typed enums, newtypes, or a focused descriptor type. It is intentionally narrower than a generic "too many strings" check and targets obvious modeling scaffolds rather than ordinary text payloads.api_redundant_category_suffixWarning. Flags paths such asuser::error::InvalidEmailErrorwhere the parent module already provides the category.api_catch_all_moduleWarning. Flags public catch-all module names such ashelpers,common, ortypes.api_repeated_module_segmentWarning. Flags repeated public nesting such aserror::error.api_organizational_submodule_flattenError. Flags public paths such aspartials::error::Errororresponse::Responsewhen the nested module is just organizational and the canonical path should drop that category layer.
The rest of the guide remains advisory. modum does not try to prove:
- which of several plausible domain decompositions reads best at call sites
- when a new module level adds enough meaning to justify itself
- when an internal long name plus a public re-export is the right tradeoff
modum reduces false negatives in two ways:
- heuristic coverage for obviously redundant leaves such as
user::UserRepository - configurable coverage for module boundaries that should stay visible via
namespace_preserving_modules
That second list should be extended per workspace for domain modules the defaults cannot infer, such as user, billing, or tenant. The defaults intentionally favor semantic surfaces like partials, page, and components over broader container namespaces like views.
Observation model:
modumreads parsed Rust source files.- It does not expand macros, resolve
include!, or prune#[cfg]. - When those constructs would weaken semantic-module family inference, it emits
api_candidate_semantic_module_unsupported_constructinstead of presentingapi_candidate_semantic_moduleas complete. - When those constructs would weaken namespace-family call-site inference, it emits
namespace_family_unsupported_constructinstead of pretending the local family witness is complete.