✦ Structural rules reference

Every structural rule Neurarch runs on your model

50 deterministic checks, no LLM in the loop. 6 of them block the agent from applying broken edits, 37 lint live as you build, and 7 compare an edit with what it was edited from. Every rule has a trigger condition you can verify against your code.

50 total rules
6 guardrail gates
37 advisor rules
7 edit checks
~3ms full pass on a 100-layer model
Layer 1

Guardrail gates (6)

These run on every action the agent proposes, before anything touches the canvas. A blocking finding requires explicit user override. Source: src/utils/agentGuardrails.ts.

G01 Impact preview warn blast radius
Trigger
Any destructive action (delete_component, delete_components_matching, rename_components_matching, clear_canvas, replace_model) where downstream impact > 4 layers, or any of them are shape-changing.
Surfaces
Number of downstream layers affected, how many would reshape, how many carry weights (rebuild required).
Override
User confirms in the impact-preview card before the action lands.
G02 Param explosion block resource
Trigger
Estimated total parameter count grows by ≥ threshold × baseline (default 5×, slider 2-20×). Blocks at 2× threshold, warns below.
Why
A misread "scale this 10×" prompt should not silently turn a 60M model into a 6B model that won't fit anywhere.
Setting
useProviderStore.paramExplosionThreshold(). Settings → "Param explosion threshold".
G03 Cycle introduction block structure
Trigger
An add_connection closes a cycle in the existing DAG. DFS check on the union of current edges and proposed edges.
Why
NN forward graphs are acyclic. Recurrence lives inside the layer type (LSTM, GRU), not as a graph cycle. A connection that closes a cycle is almost always a mis-step.
Source
DAG requirement matches PyTorch nn.Module.forward semantics. torch.autograd docs note that gradient computation requires a DAG.
G04 Orphan layers warn structure
Trigger
add_component without afterName, and no follow-up add_connection referencing the new component as either endpoint.
Why
A new layer that's not wired is dead code. The agent occasionally proposes these when it forgets to chain edits; the warning flips the decision back to the user.
G05 Shape inference (new) block shape
Trigger
Propagates tensor shapes through the sandboxed post-action graph and surfaces only the issues the action would introduce. Sub-checks: attention embedDim % numHeads, GQA numHeads % numKVHeads, elementwise merge parent equality, concat axis compatibility, explicit linear inFeatures vs upstream, computeOutputShape throw, and outputs with NaN / 0 / negative dims at the first layer to introduce them.
Source
Per-layer transforms from componentRegistry.computeOutputShape. Head-dim convention from Vaswani et al. 2017 (Attention Is All You Need). GQA ratio from Ainslie et al. 2023 (GQA).
Why
Text-layer review tools (Cursor, Copilot) can't catch embedDim=384, numHeads=5 until the GPU rejects the kernel. This gate fires sub-millisecond, pre-apply.
G06 Param range warn param-range
Trigger
A single parameter value falls outside its conventional range on add_component or update_params: e.g. dropout above 1, stride of 0, numHeads of 0. Uses the same convention-based ranges the inspector shows inline.
Why
These are values the shape gate cannot see: the graph still propagates, the model still builds, and the run is quietly wrong. A dropout of 1.2 is not a shape error, it is a silently dead layer.
Source
src/utils/paramConstraints.ts::validateParamValue, shared with the inspector so CI and the canvas never disagree.
Layer 2

Architecture advisor rules (35)

These run live as you build, every model change, no agent involvement. They surface as inline warnings on the canvas and in the Advisor panel. Source: src/utils/architectureAdvisor.ts.

R01 No Input node block structure
Trigger
Model has ≥ 1 component but no component of type input.
Why
Without an Input layer, tensor shapes can't be propagated and generated code is incomplete.
Fix
Drag an Input layer from the I/O section of the palette.
R02 No Output node warn structure
Trigger
Model has > 1 component but no component of type output.
Why
Code generator can't tell where the forward pass terminates.
R03 Isolated components warn structure
Trigger
Component has no inbound and no outbound connections.
Why
Most often a stranded layer left over from a refactor. Code generator skips it but it inflates the visual graph.
R04 Dead-end layer warn structure
Trigger
A non-output node has inbound connections but no outbound connections.
Why
Compute happens but the result is discarded. Gradients flow into a black hole.
R05 BatchNorm / LayerNorm after activation warn ordering
Trigger
A normalization layer is connected directly downstream of an activation (relu, gelu, swish, etc.).
Why
Normalization is meant to stabilize the pre-activation distribution. Applying it after activation breaks the assumption and shifts already-nonlinear features back toward zero mean.
Source
Ioffe & Szegedy 2015, BatchNorm places BN between Linear/Conv and the activation.
R06 Dropout directly before BatchNorm info ordering
Trigger
A dropout layer is connected directly upstream of a BatchNorm.
Why
Dropout at train time changes the activation variance; BN's running statistics get distorted. A known train-vs-eval mismatch.
R07 Softmax / Sigmoid directly before Output info ordering
Trigger
An explicit Softmax or Sigmoid layer is wired directly into Output.
Why
PyTorch's nn.CrossEntropyLoss applies LogSoftmax internally; an explicit Softmax before it double-applies and slows training. BCEWithLogitsLoss has the same issue with Sigmoid.
R08 Normalization at output warn ordering
Trigger
A normalization layer is wired directly into Output without a final linear / classification head.
Why
Normalization zero-centers the logits, breaking calibration. The model's output scale is no longer meaningful.
R09 Deep network with no residual connections warn pattern
Trigger
≥ 8 weight-carrying layers (conv2d / linear / etc.) and zero residual / skip / add nodes.
Why
Gradient signal degrades through depth without skip connections.
R10 Attention without positional encoding warn pattern
Trigger
Model contains attention layers but no positional encoding (sinusoidal, learned, RoPE, ALiBi).
Why
Self-attention is permutation-invariant. Without PE, the model is a bag-of-words and can't learn order.
R11 Sigmoid / Tanh in deep networks info pattern
Trigger
Sigmoid or Tanh activation appears in a network with ≥ 5 weight-carrying layers.
Why
Saturating activations vanish gradients in deep stacks. ReLU / GELU / SiLU are the modern defaults.
Source
Glorot & Bengio 2010 on the vanishing gradient problem.
R12 Deep network with no normalization info pattern
Trigger
≥ 7 non-I/O layers and zero normalization layers (BN / LN / IN / GN / RMSNorm).
Why
Without normalization, deep nets converge slowly and are sensitive to initialization scale.
R13 Very high dropout rate warn performance
Trigger
Any dropout layer with p > 0.65.
Why
Common dropout rates are 0.1 to 0.5. Anything above 0.65 usually indicates a typo or a confused regularization strategy.
R14 Very large activation tensor warn performance
Trigger
Any layer's estimated activation tensor exceeds 50 M elements (~200 MB per sample at float32).
Why
Activations live in GPU memory through the backward pass. One bloated layer forces tiny batch sizes or OOM.
R15 MoE without auxiliary loss info pattern
Trigger
Model contains an MoE layer but no auxiliary load-balance loss is referenced.
Why
Without an aux loss, experts collapse into a few dominant ones. Most papers add a 0.01 weight router-balance term.
R16 GQA numHeads not divisible by numKVHeads block structure
Trigger
A groupedQueryAttention component has params where numHeads % numKVHeads != 0.
Why
GQA groups query heads; the group size must divide the head count. Mis-set ratios crash on the first attention forward.
R17 SwiGLU intermediateSize convention info performance
Trigger
A swiGLU / gated FFN has intermediateSize that doesn't follow the common ~(2/3) × 4 × hidden convention used in LLaMA / Mistral.
Why
Param-count parity with standard FFN; mis-sized SwiGLU silently changes the FLOP and param budget.
R18 Conv feeds Linear with no flatten / pool block structure
Trigger
A convolution (conv1d/2d/3d or a depthwise/separable/transpose variant) connects directly into a linear layer.
Why
Conv outputs a multi-dimensional feature map; Linear expects a flat [batch, features] tensor. The forward pass raises a shape error, or silently mis-multiplies the spatial dims. Insert a Flatten or a Global Average Pool between them.
Source
Standard CNN classifier construction (e.g. LeNet / AlexNet head).
R19 Back-to-back activations warn ordering
Trigger
An activation node (relu/gelu/silu/sigmoid/tanh/softmax/…) connects directly into another activation node with no Linear/Conv/Norm between them.
Why
Two activations in a row add compute but no expressivity (same-type is a no-op duplicate). A common slip is ReLU → Softmax, which clips logits to ≥ 0 and distorts the output distribution.
Source
Composition of pointwise non-linearities adds no representational power without an intervening affine map (universal-approximation premise).
R20 Linear → Linear with no activation info pattern
Trigger
A linear layer connects directly into another linear layer with no activation between them.
Why
Two stacked linear maps collapse into one (W₂·W₁), so the extra layer costs parameters but adds no representational power, usually a forgotten ReLU. Deliberate low-rank / factorized projections (down-proj → up-proj) are the safe exception.
Source
Linear-map composition closure; the rank of the product cannot exceed the smaller intermediate dimension.
R21 Dropout immediately before Output warn ordering
Trigger
A dropout layer is the last node before the output node.
Why
In training this randomly zeroes the final logits fed to the loss; at eval it is a no-op, so train and eval behaviour diverge. Dropout belongs before the final projection (Linear/Conv), not after it.
Source
Srivastava et al. 2014 (Dropout): dropout regularizes hidden representations, not output logits.
R22 Conv stride larger than kernel warn structure
Trigger
A downsampling convolution (conv1d/2d/3d, depthwise, or separable) has stride > kernelSize.
Why
The kernel jumps past its own footprint each step, so a band of the input is never read, a silent loss of information. Non-overlapping patches use stride == kernel (e.g. ViT 16/16); stride > kernel is almost always a typo.
Source
Convolution arithmetic: Dumoulin & Visin 2016.
R23 Non-spatial tensor into Conv warn structure
Trigger
A flatten or linear layer connects directly into a (non-transpose) convolution.
Why
Mirror of R18: a Conv needs a [channels, …spatial] feature map, but Flatten / Linear emit a flat [batch, features] vector. The forward pass raises a shape error unless the spatial dims are rebuilt with a Reshape / Unflatten first.
Source
PyTorch nn.Conv*d input-shape contract.
R24 Back-to-back normalization info pattern
Trigger
A normalization layer connects directly into another normalization layer (e.g. LayerNorm → BatchNorm).
Why
Normalizing an already-normalized tensor is redundant; the second layer mostly re-centres/re-scales what the first produced and just burns its own learnable parameters.
Source
Idempotence of standardization: Ioffe & Szegedy 2015.
R25 Duplicate positional encoding info pattern
Trigger
More than one positional-encoding layer (positionalEncoding, learnedPositionalEmbedding, rope, alibi) in the model.
Why
Position is normally injected once. Stacking absolute + rotary, or two of the same, double-counts position and tends to hurt more than help.
Source
Positional-encoding design: Vaswani et al. 2017, RoPE (Su et al. 2021).
R26 Pooling feeds Linear with no flatten warn structure
Trigger
A spatial pooling layer (maxpool, avgpool, adaptive pool) connects directly into a linear layer. Global pools are exempt: they already collapse spatial dims.
Why
Sibling of R18: pooling still emits a [channels, …spatial] map, but Linear expects a flat [batch, features] tensor, so the forward pass raises a shape error. Insert a Flatten or a Global Average Pool first.
Source
Standard CNN classifier head construction.
R27 Flatten before attention warn structure
Trigger
A flatten layer connects directly into an attention layer.
Why
Flatten collapses the sequence dimension into one long vector, leaving a length-1 sequence, so attention has nothing to relate. Keep the [sequence, dim] layout and flatten only after the attention stack.
Source
Attention operates over a sequence axis: Vaswani et al. 2017.
R28 ConvTranspose checkerboard risk info pattern
Trigger
A ConvTranspose2d has kernelSize not divisible by stride (with stride > 1).
Why
Uneven kernel overlap during upsampling deposits more weight on some output pixels than others, producing checkerboard artifacts. Make kernelSize a multiple of stride, or upsample with Upsample + Conv (resize-convolution).
R29 GroupNorm channels not divisible by numGroups block structure
Trigger
A GroupNorm layer's channel count is not an exact multiple of numGroups.
Why
GroupNorm splits channels into equal groups; a non-divisible count raises at construction. Parallel to the GQA / attention head-dim divisibility checks. Set numGroups to a divisor of the channel count.
R30 Very large Linear layer warn performance
Trigger
A single Linear exceeds ~1B parameters (inFeatures × outFeatures).
Why
A dense layer that large (~4 GB float32) almost always means a feature map was flattened without pooling first. Add a Global Average Pool / more downsampling, or factorize the layer. Embedding and vocab-projection heads are the expected exception.
Source
Parameter-budget hygiene; a single matrix this size dominates model memory.
R31 Full multi-head attention at LLM scale info performance
Trigger
A transformer stack (≥6 attention layers) at LLM width (embedDim ≥ 2048) uses full multi-head attention everywhere, with no grouped-query or latent attention present.
Why
At real model scale the KV cache, not the weights, dominates serving memory. Full per-head K/V caching is what production LLMs move away from: grouped-query attention (e.g. 8:1) cuts the cache ~8×, and multi-head latent attention (MLA) shrinks it ~10× or more, without changing the parameter count. Advice, not a bug: set numKVHeads below numHeads, or switch to mla.
Source
Serving-cost roofline; the same KV-cache math the app and the public serving calculator run.
R32 Default init assumes ReLU, saturating activation follows info pattern
Trigger
A Linear/Conv layer feeds a sigmoid or tanh activation directly.
Why
PyTorch's default Kaiming (He) init is derived for ReLU-family activations. Feeding a saturating activation from a He-initialized layer starts training in the saturated tails and shrinks early gradients. Use Xavier init with the matching gain, or a ReLU-family activation.
Source
Glorot & Bengio 2010 (Xavier) vs He et al. 2015 (Kaiming) derivation assumptions.
R33 Deep attention stack without depth-scaled init info pattern
Trigger
≥ 8 attention layers stacked in one model.
Why
Residual-branch outputs add up with depth; unscaled init lets activation variance grow layer over layer. GPT-2/LLaMA-family models scale residual output projections by depth: N(0, 0.02 / √(2L)).
Source
GPT-2 (Radford et al. 2019) init convention; carried forward by LLaMA/Mistral.
R34 LM head width disagrees with embedding vocab info structure
Trigger
The model has an embedding with a numeric vocabSize and attention layers, but the final Linear feeding the Output projects to a different width.
Why
A language model's head must project back to vocab size (and is usually weight-tied to the embedding); a mismatch means the model cannot emit token logits. A classifier head over N classes is the expected exception, which is why this is info, not error.
Source
Weight-tying convention (Press & Wolf 2017); standard LM head contract.
R35 KV cache exceeds serving budget at reference context warn performance
Trigger
Total fp16 K/V cache across all attention layers (GQA-aware: counts numKVHeads, skips MLA) exceeds 4 GB for a single 8,192-token sequence.
Why
That much cache for ONE sequence is gone before weights or activations load; on a 24 GB GPU it caps concurrency at a handful of requests. Raise the GQA ratio, switch to MLA, reduce depth/width, or accept a shorter serving context.
Source
Same per-token KV math as R31 and the serving calculator, applied as an absolute budget instead of a pattern.
R36 Predictive branch with no stop-gradient on the target warn pattern
Trigger
Model contains a JEPA predictor but no emaTarget node and no branch carrying stopGradient.
Why
Joint-embedding predictive objectives have exactly one degenerate solution: emit a constant. The momentum target that carries no gradient is the only thing ruling it out, so a gradient-carrying target is not a slower design, it is one whose loss reaches zero without learning anything.
R37 Matryoshka prefix wider than the embedding block structure
Trigger
A matryoshkaHead lists a nestedDims entry greater than its own embedDim.
Why
The head's entire contract is that embedding[:d] is usable on its own for each listed d. A d larger than the width names a prefix that does not exist, so a caller who truncates to it silently gets the full width instead and indexes at a size nobody chose. There is no reading of the graph under which a prefix longer than the whole was meant.
Layer 3

Edit checks (7)

These compare a change with what it was changed from: two graphs, two config.json files, or an adapter's target_modules against the model it attaches to. Each one catches an edit that loads and runs and is wrong, so nothing raises for an agent to react to. They run in POST /api/v1/plan (send base, config + base_config, or a spec with an adapter) and in the pull request comment. Source: src/utils/editChecks.ts.

C01 Vocabulary resized, a token-indexed tensor was not block edit
Trigger
Compared with the base, the input embedding (or vocab_size in config.json) changed size, and another tensor with the old vocabulary as its row count kept it: a per-layer embedding (embed_tokens_per_layer, vocab_size_per_layer_input), an output head, or a second copy of vocab_size in a nested text_config.
Why
New token ids index past the end of every tensor that kept the old size. The resize and the save both succeed and nothing raises, so an agent that iterates on errors has nothing to iterate on.
Seen in
transformers#45276 (Gemma 4 resize_token_embeddings skips embed_tokens_per_layer and the output embeddings), transformers#39921 (Gemma 3n).
C02 Weight tying lost in the edit warn edit
Trigger
The base ties the output head to the input embedding (tie_word_embeddings: true, or a head with tied) and the changed model sets it false, drops the key, or keeps the tie on a head whose shape no longer matches the embedding.
Why
An untied head is a second vocab × hidden matrix that drifts from the embedding from the first step. When the flag is dropped on save, the reload decides by class default and comes back untied with no error.
Seen in
transformers#38160 (tie_word_embeddings not saved on a customised model), peft#2944.
C03 Attention config keys that did not move together warn edit
Trigger
In config.json: head_dim equalled hidden_size / num_attention_heads in the base and stayed put while one side moved; or num_attention_heads changed and num_key_value_heads did not, which silently changes the query-group size (or turns multi-head attention into grouped-query attention). A change that breaks divisibility is reported under G05 / R16 instead.
Why
An explicit head_dim is honoured as written, so the projections build at a width nobody chose and the model runs. Divisibility failures crash; these do not.
C04 Context length moved without RoPE scaling warn edit
Trigger
In config.json: max_position_embeddings was raised and the RoPE scaling factor (rope_scaling or rope_parameters) did not change, the factor changed and the length did not, or both moved by different ratios.
Why
Positions past the trained range are extrapolated rather than interpolated, or the extension is encoded but never used because runtimes size the window from max_position_embeddings.
Seen in
Qwen3-30B-A3B discussion #7 (rope keys for vLLM).
C05 LoRA target_modules match nothing block adapter
Trigger
The change spec's adapter.targets (PEFT target_modules: a list, a regular expression, or all-linear) matches no Linear, Embedding or Conv module in the graph, matched the way PEFT matches: full name equal to the entry or ending in .entry.
Why
An adapter with nothing to attach to trains zero parameters. PEFT raises on the first adapter; a second adapter on the same model is accepted silently.
Seen in
peft#3533 (inject_adapter silently accepts a second adapter whose target_modules match nothing).
C06 LoRA targets reach only part of the model warn adapter
Trigger
Some entries of target_modules match nothing while others match, or an attention projection is matched in some layers and missed in the rest.
Why
PEFT only errors when every entry misses. A typo or another architecture's module name in a list of five trains on the other four without a word.
Seen in
C07 LoRA target is not a Linear warn adapter
Trigger
A target_modules entry matches a module PEFT cannot inject into: a class that holds a Linear without being one, a whole attention block, a norm.
Why
PEFT supports nn.Linear, nn.Embedding, nn.Conv1d/2d/3d and transformers Conv1D. A wrapper is refused or skipped; target the Linear inside it by its own name.
Seen in
peft#3129 (Gemma4ClippableLinear inherits from nn.Module, not nn.Linear).

What opt-in corpus reporting sends

The neurarch-lint GitHub Action (report-corpus: true) and the neurarch-mcp server (NEURARCH_REPORT=1) can share one anonymous structure+verdict row per linted model. Both are off by default.

Sent: the structural fingerprint (an 8-character hash of the layer-type histogram and edge count), the histogram and edge count themselves (which let the server verify the fingerprint is real), and the finding pairs, rule id plus severity.

Never sent: the model graph, parameter values, layer names, code, file paths, the repository identity, or any user or machine identifier. The rows are unauthenticated by design; there is no way to attribute one to a person. The server rejects any row whose fingerprint cannot be recomputed from the histogram it came with.

Reliability: reporting has a 5-second timeout and swallows every error. It can never fail a build or a tool call.

Try it on your own model

Paste a HuggingFace ID or drop a .py file. The same 40 rules run client-side, and now also run in CI on raw .py via the neurarch-lint GitHub Action, sub-millisecond.

Open Neurarch →