Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Architecture Rules And The Debt Ratchet

Nervix separates facts about Rust source from facts about the APIs a compilation resolves. The source checks inspect inactive code too. The compiler checks identify definitions, normalized types and source-owned contracts in each declared legal configuration. A method name alone is never evidence of its owner or effect.

just ratchet runs both owners. just validate and just validate-ci require the same gate; formatting is applied by the former and checked by the latter. The compiler emits ordinary nervix:: diagnostics. The four structural counts compare with debt-baseline.json; a count may decrease and cannot increase. just ratchet --update records a decrease only after the complete gate passes. No numeric allowance controls a semantic rule.

Rule Contracts

The fifteen rule families below have one implementation each. The family name remains useful for describing its purpose; a compiler family can emit more than one diagnostic where a missing contract and a violated contract require different corrections.

FamilyPurpose and owning factAllowed exampleRejected exampleEnforcement and qualification
data_plane_lock_acquisitionsResolve synchronization APIs and the execution contract of their callers. Concurrency owns contexts, effects and bounds.A task-local HashMap::entry; a registry acquisition with a lifecycle contract.A recurring callback reaching a DashMap shard or a lifecycle-only helper.Compiler sync_acquisition, lifecycle_call, unknown_effect; resolved receiver, helper, callback, trait, macro and expectation fixtures.
bare_error_signaturesA Nervix failure contract carries its complete contextual report. Errors owns outcome, library and foreign-interface classifications.Result<T, Report<DomainError>>; an exact reason-bearing outcome contract.An owned failure in an alias, inferred future or stream without its report.Compiler bare_error_signature; associated-output, alias, inference, metadata and report-carrier fixtures.
discarded_resultsPreserve a failure or classify recovery at its consuming operation.A propagated failure or the recovery owner’s explicit Discarded operation.A failed outcome discarded through a binding, projection or drop.Compiler discarded_outcome; complete and partial-discard, borrowed, tuple and collection fixtures.
bare_unwrap_and_expectA canonical Option/Result panic needs a construction or checked guarantee, expressed through meticulous.verified after checking the condition; an unrelated object’s method named expect.Canonical unwrap or expect, including aliases and UFCS.Compiler bare_panic; canonical/custom methods, dereference and call-form fixtures.
model_matches_in_data_planeConfiguration Models stay inside their owning boundary; data plane and connector engines consume plans.A vocabulary Model or a typed connector plan.A forbidden Model in a body, normalized interface or nested aggregate.Compiler model_in_data_plane; source layer, value and input contracts, cross-crate and projection fixtures.
parser_imports_outside_edgesLanguage dependencies belong to the language and its declared edges.The session adapter consuming public parsing; a runtime consuming Models.A language reference outside its authorized layer, including aliases.Compiler parser_dependency plus disjoint inactive import/manifest provenance; qualified names, reexports and dependency fixtures.
data_plane_cluster_awaitsRecurring processing retains observations instead of awaiting cluster control access.A lifecycle observation; reading an installed immutable publication.Recurring work awaiting a declared cluster-access capability.Compiler cluster_await; direct, bound-future, helper and trait fixtures.
string_node_idsDeclared node identities use the shared typed vocabulary. Typed States owns the identity contract.ClusterNodeId; an ordinary string unrelated to node identity.A declared identity interface or field represented by a string.Compiler node_identity; alias, interface, aggregate, field and metadata fixtures.
write_once_rwlock_fieldsInstallation-only state is an installed value rather than replaceable locked absence.An installed value; a genuinely mutable field with its lifetime reason.An installed field containing an RwLock, or an unclassified candidate requiring review.Compiler installed_state and state_contract; normalized representation, lifetime, review and exception fixtures.
clamped_arithmeticThe resolved primitive integer or Duration API chooses saturation or wrapping instead of checked overflow.Checked arithmetic; pointer wrapping; an Instant elapsed-time query; a specifically justified retry ceiling or modular mixer.An integer/Duration saturating or wrapping operation without its actual arithmetic meaning.Compiler clamped_arithmetic; integral and Duration owners, aliases, UFCS, callbacks, authored macros and narrow-expectation fixtures.
combinator_control_flowCanonical Option/Result adapters must not hide a branch or a sequence.One transformation, an iterator pipeline, or an unrelated custom method.map_or/map_or_else, successive governed adapters, or a governed adapter with a closure containing statements.Compiler combinator_control_flow; canonical/custom receiver pairs, UFCS, closure structure, diagnostic cardinality and source spans.
as_castsCount actual Rust cast expressions, not words in imports, attributes or qualified paths.use Value as Alias; r#as; <Type as Trait>::item; ordinary literals.An expression using as, including a const expression nested in a qualified type.Stable source parser; inactive cfg, raw identifiers, nested syntax, macro fragments and bounded source properties. Clippy independently denies conversions in its broader build scope.
testing_feature_struct_fieldsA product struct has one field shape rather than a shape selected by testing.A test-only item, an API gate or a conditional documentation attribute.A named or tuple struct field whose presence depends on testing, including a conditional cfg attribute.Stable source parser; field ownership, tuple, inactive, compound-predicate and cfg_attr controls. Enum fields are outside this specific struct rule.
files_over_3000_linesKeep product and in-crate test files within a source-size bound.At most 3,000 lines, counting a final line without a newline.An eligible file with more than 3,000 lines.Source discovery and physical line count; exact boundary and newly authored source tests.
functions_over_complexity_30Keep each function’s decisions within a cyclomatic complexity of 30. At full coverage a function’s CRAP score equals its complexity, so a function above 30 exceeds a CRAP threshold of 30 at any coverage. Function complexity owns the analyzer and its limits.A function the pinned analyzer scores at most 30; an existing function above 30 that grows, which keeps the count.A function newly scored above 30, including one in an inactive configuration or an in-crate test file.Pinned cargo-crap analysis over source discovery; exact 30/31 boundary, inactive configuration, macro, trait and nesting limits, eligibility, and missing, failing, other-version, skipped-source, empty, configured, undiscovered and unreadable-file fixtures.

The Option/Result adapter catalog covers and_then, filter, map, map_err, map_or, map_or_else, ok_or, ok_or_else, or_else, unwrap_or, unwrap_or_default and unwrap_or_else. It resolves their inherent owner. A custom map_or is not a canonical branch, and an Iterator’s map is not an Option/Result adapter. A chain is diagnosed once at its outer governed call; a later binding separates steps. A closure’s statement structure is a source fact, not a claim that the compiler understands the business meaning of its transformation.

Arithmetic recognition uses the defining crate and inherent primitive integer/Duration owner. Type aliases and UFCS preserve that identity. A function reference retains the arithmetic diagnostic before a callback or function-pointer type can erase its definition. A wrapper’s authored body is checked when its configuration compiles; matching words on the wrapper do not grant it permission or make it a canonical API. Instant’s saturating elapsed-time query is a query with a defined zero result, and pointer wrapping is pointer arithmetic. Neither is the integer/Duration overflow choice this family enforces.

Scope And Configuration

Compiler coverage is precisely tools/nervix-lint/configurations.toml: the ordinary workspace’s libraries and binaries, browser root, testing library and separate legal Shuttle, Loom, Turmoil and Deloxide configurations. The diagnostic variants have their own library and binary invocations. They are never combined with --all-features. Every expected target and report must be present, current and complete. Authored macro expansion and captured caller tokens retain provenance; generated build-directory code and external definitions are explicit exclusions. The compiler does not infer the APIs of code that no declared configuration compiles.

Source discovery includes tracked Rust and newly authored Rust not ignored by Git. The four counts retain their product-debt scope: directory components named tests, benches or examples are excluded. In-crate tests.rs, *_tests.rs and test_fixtures.rs count toward file size and function complexity but are excluded from cast/field debt. Test-only items are excluded from those two syntax counts. Other inactive configuration branches remain visible. These exclusions belong to these counts; they do not exempt a file from the primitive, manifest, clock or language provenance validators, which own their broader authored-source contracts.

The stable source parser reads original Rust text and walks cast expressions and attributes attached to actual struct fields, retaining token locations even beside Unicode comments. It also parses Rust fragments in authored macro groups after replacing metavariable markers with their identifiers, preserving token locations. Repetition bodies are visited independently. Opaque macro DSL words and invalid fragments are not reinterpreted as Rust expressions. An invalid top-level Rust file fails with its owning path and location. This does not expand a macro or prove which tokens an external procedural macro would generate.

The parser requires no rustc internals or analysis driver. just typed-ratchet-source-build builds it on the stable toolchain. Native compiler coverage builds it on that producer’s pinned toolchain so LLVM profiles and exporters match. The tooling workspace stays outside product dependencies, including browser and ordinary release graphs.

Function Complexity

functions_over_complexity_30 counts the functions whose cyclomatic complexity exceeds 30, as one pinned analyzer measures it: cargo-crap at the version scripts/cargo_crap.py declares, the same build the advisory CRAP report of just check-coverage runs. just install-cargo-crap builds that version with cargo install --locked into target/tools/cargo-crap-<version>/, through the compiler wrapper the host configures, and keeps a build that is already there. just ratchet, just test-ratchet-units, just check-coverage, just test-crap-report and just coverage-crap-report-runner depend on it, and the CI checks, coverage and extra-tests jobs build it through kache. A binary that reports another version is refused with the command that installs the pinned one; a different version in PATH is never used.

The complexity is classical McCabe: one, plus one for every if, for, while, loop, match arm, &&, || and ?. A function at exactly 30 is not counted and one at 31 is. Growing a function that already exceeds 30 leaves the count unchanged; a new function above 30, or one that crosses it, raises the count.

The analyzer walks the repository root with the eligibility above. It reads syntax and expands nothing, so these are outside the count:

  • a #[test] function and everything inside a #[cfg(test)] module;
  • a function inside a macro, whether an authored macro_rules! body or an invoked item macro such as cfg_if!;
  • the body of a trait’s provided method;
  • a function nested in another function’s body, and the decisions of a closure, which belong to no reported function.

Fixtures pin each limit, so a new analyzer version that changes one fails a test. Inactive configuration branches stay visible.

Missing analysis never reads as zero debt. The count fails, naming the cause, when the pinned binary is missing or reports another version; when the analyzer exits with an error or writes a warning, which it does only for a source it could not read or parse; when it measures no function; when its report has another version or a ? weight other than one; when a .cargo-crap.toml in the repository root or above it could change the analysis; when it measures a file source discovery does not govern, such as one in a nested repository; and when a discovered file is hidden, a symbolic link or tracked but ignored by Git, which the analyzer’s walk would skip.

just ratchet --show functions_over_complexity_30 lists each function’s file, line, name and complexity. The baseline holds counts rather than sites, so when a count rises the gate lists every site of that count and the function a change added is named among them. Decomposing a function is the repair. Extracting a forwarding wrapper only to move the number is the workaround just lint-audit attributes to a lowering of this count.

Exceptions And Ownership Review

A semantic exception is a reason-bearing expect at one operation or binding, gated through cfg_attr(nervix_lint, ...) or nervix_primitives::expect_lint!. A module or function allowance, blanket warning suppression, missing reason, unused expectation or expectation covering another operation fails. Arithmetic reasons state the actual ceiling, capacity or modular computation; “cannot overflow” does not justify wrapping. A retained defect names its repair task.

Source counts have no semantic exception channel. A current structural occurrence remains counted until its owner changes it; a compiler expectation cannot hide its inactive source. Likewise, source provenance cannot be waived by declaring a lifecycle or error contract.

Review still owns the truth of reasons, lifetime classifications, architectural roles and bounds. The compiler checks their placement, shape and supported effects, not arbitrary whole-program ownership. Finite API recognition does not establish business semantics for an unrelated custom method, an erased indirect transformation or an opaque dependency. A paired rejection fixture proves the specified route; it does not prove every possible alternative absent.

Authoring And Qualification

Add a semantic rule in Rust beside its owner. Qualify an allowed/rejected pair that differs in the semantic fact being enforced, not just a name. Include aliases, call forms, macro origin, caller or metadata effects when applicable, exact diagnostics and source locations, and invalid, unfulfilled and widened expectations. Change the receiver or definition to an unrelated object as a false-positive control; restore the canonical API through an alias or UFCS as a false-negative control. Source rules also qualify inactive branches and lexical distractions.

Use just test-ratchet-units, just test-typed-ratchet-reports, just test-typed-ratchet-compiler, just test-typed-ratchet-contracts and just test-typed-ratchet for the source, report, ordinary and separate mode qualification. just typed-ratchet --inventory --show displays a calibration without claiming the diagnostic gate passed. just ratchet requires the gate. Cache qualification remains just qualify-typed-ratchet-cache; changed source, annotations, rules, dependencies and configuration invalidate evidence.

The registered typed-source-syntax Bolero property uses bounded current Rust syntax, complete source input/site equality and deliberate cast/field counts. Ordinary corpus/randomized checks are required; just fuzz typed-source-syntax is actual sanitizer-backed fuzzing, and CI runs that mode only for a PR labeled fuzz. A sanitizer skip supplies no fuzz or coverage evidence. Compiler mode qualification checks analysis, not execution of the runtime’s concurrency models. The sanitizer compiler adapter preserves the source parser’s span contract as described in Property Testing And Fuzzing. The concurrency, error and state chapters retain those operational owners.

Auditing Lint Workarounds

A rule that misjudges code invites code shaped around the rule. The textual counters this gate replaced matched method spellings inside named files, so lowering a count sometimes meant respelling an operation or moving it rather than changing what the rule was about. just lint-audit finds code shaped like such a workaround and attributes it to the history that produced it.

The compiler pass records review candidates beside its acquisition findings in every complete analysis, so the audit reads the evidence just ratchet uses and runs no second analysis. just lint-audit --inventory audits with warning levels while the gate still has open diagnostics, and --configuration selects declared configurations as an inventory. Each candidate carries its owning body, that body’s execution contract (declared, or recurring by reachability from a recurring caller), its spans and macro origins, and the configurations that compiled it.

KindResolved patternControls that are not candidates
repeated_lookupKeyed operations (get, get_mut, contains_key, insert, remove, entry, indexing and their IndexMap forms) of a standard, hashbrown, IndexMap, DashMap or JSON map collection, on one receiver place with one key value, where a later operation can run after an earlier one. guarded marks a later operation in a branch the earlier result selected.Exclusive branches, a receiver or key assigned between the operations, different keys or collections, and another type with map-like method names.
key_allocationA borrowed lookup handed an owned copy of its key, or an entry handed a copy that a hit discards.Borrowed keys, owned insertion keys, Copy values, shared-handle clones and formatted keys.
borrowed_copyAn owned copy whose only use is a borrow, such as &value.clone() or text.to_string().len().Copies that are moved, Copy values, shared-handle clones and text formatted from another value.
duplicated_branchA call or assignment that every branch of one exhaustive if chain or match repeats, with the same resolved callees, generic arguments and type.An if without else, exits, constructed values, and arms that dispatch one spelling over differently typed bindings.
forwarding_wrapperA synchronous function whose whole body passes its parameters unchanged and in order to one local function, with its caller count and whether it carries a source contract, implements a trait method or calls into another file.Transformed, reordered or partial arguments, library callees and constructors.
discarded_errorResult::ok, unwrap_or, unwrap_or_default, is_ok, is_err, an unwrap_or_else, map_err or or_else closure that ignores its failure, an Err(_) arm, if let Ok without else and let Ok(..) = .. else, when the failure carries information.Unit, uninhabited and zero-sized failures, and failures handed on.
disguised_clampChecked primitive-integer or Duration arithmetic, or a checked integer conversion, whose absence falls back through unwrap_or, unwrap_or_default or unwrap_or_else.Overflow decided by its own statement.
relocated_acquisitionA synchronization acquisition outside the files the textual lock counter read, written by a commit that lowered that count.Acquisitions in the counter’s files, and ones no lowering commit wrote.

Attribution blames each candidate’s lines in the working tree, reads the diff hunk of the commit that completed them, and replays the history of debt-baseline.json. For every count the candidate’s kind could have worked around, it records whether that commit introduced or lowered the count, whether its hunk, the rest of the candidate’s file or the commit’s other files removed the counter’s governed spelling (such as .entry( for the lock counter, counted only in the files that counter read at the time), and whether the code predates the count. The report keeps the hunk’s removed and added lines around the candidate’s line. When the completing commit only carried the pattern forward, the earlier commits of the same lines are read for that evidence and the one found is recorded as the origin. --pull-requests also reads, through gh, the commits a squash merge folded away, finding its pull request by the number the subject names or, for a subject without one, by the merged commit, and records a commit that removed the spelling while writing one of the candidate’s own lines. A read that ends with a server error or a dropped connection is attempted up to four times with a doubling backoff; a failure that persists, or any other failure, ends the audit without a report.

The suggestion is suspected when a count event coincides with a spelling removed from the candidate’s own hunk or file, or a folded commit rewrote the candidate’s lines away from it. A removal in another file explains the candidate only for the lock counter, which read named files, so moving an operation out of them lowered it. The suggestion is possible when only an event or a removal is present, or when the code was written into a file the lock counter read before the compiler resolved that count, since such code could avoid the spelling without changing the count; unattributed when the code was written while a count existed without any of these; pre-existing when it predates every plausible count; and uncommitted for lines not yet committed. A suggestion is evidence for review, never a classification: chronology alone proves nothing.

Candidate identities hash the kind, file, owner, primary source and pattern facts without byte offsets, so an unrelated edit keeps them, and a repeat within one owner takes an ordinal. audit.json and audit.md land in target/lint-audit/<source identity>/. They are generated evidence: review dispositions, inventories and measurements belong on the owning task, and nothing reads them back as policy.

Review classifies each candidate as confirmed lint-caused, suspected, unrelated, pre-existing or intentional, recording the source revision, owning rule, before and after code, reason and disposition. A repair keeps lazy and fallible initialization, side-effect counts, borrowed-key hit behavior, exact equality, lifetimes, reentrancy, error and acknowledgement semantics and ordering. An entry is not a repair where a hit would discard an allocated key, where a callback runs between the lookup and the insertion, or where the miss path needs the whole collection. Fewer searches do not make an entry faster either: on a small ordered map whose hits dominate, a lookup can cost less than building the entry, so the measurement covers hit-heavy and miss-heavy key distributions. A performance claim needs measured lookup, comparison and allocation counts and same-host Criterion or just benchmark-ab evidence; a shorter function or a lower count is not a speedup.