Guard
The invariant layer — every rule Mandu's static analyzer enforces, what each one means, and how to fix the violation.
On this page
Guard
Guard is the reason agents can commit to your repo without torching it. It is a static analyzer that runs at build time (and on every save in dev), walks your imports, and rejects anything that violates the architectural contract.
This page is the invariant catalog. If Guard ever yells at you, the fix is on this page.
What Guard actually does
On every watched file (.ts, .tsx, .js, .jsx, .mjs, .cjs), Guard
extracts every import, import(), and require() statement (source:
guard/analyzer.ts:64-132), resolves each one to a layer, and checks the
layer graph against the active preset.
Six families of violations can be produced (source: guard/types.ts:191-197):
| Type | What it means |
|---|---|
layer-violation |
Import crosses a forbidden layer boundary |
circular-dependency |
A → B → A detected |
cross-slice |
Same layer, different slice, imported directly |
deep-nesting |
Reaches past a slice's public API |
file-type |
.js or .jsx file found in a scanned layer |
invalid-shared-segment |
Path under src/shared/ that is not on the allow-list |
FS-routes and contract/slot rules layer additional checks on top of that (more below).
The layer model
Each layer is a { name, pattern, canImport, description } tuple (source:
guard/types.ts:116-128). The Mandu preset defines layers like
client/widgets, client/features, server/infra, shared/contracts,
shared/utils/client, shared/utils/server, and so on. A file is assigned
to a layer by matching its path against each layer's glob pattern (source:
guard/analyzer.ts:152-166).
An import is allowed iff:
importedLayer === importingLayer
OR
importedLayer ∈ importingLayer.canImport
Two wrinkles applied before that check:
shared/envis server-only. Any import fromshared/envin a file whose layer starts withclient/is rejected with severityerror(source:guard/validator.ts:277-287).- Same layer, different slice — allowed for imports that resolve to the
layer's public API; direct deep imports trigger
cross-slice.
The invariant catalog
layer-violation
Trigger: the importing layer's canImport list does not include the
imported layer.
// src/client/widgets/chart/Chart.tsx
import { db } from "@/server/infra/db"; // ✗ client/widgets cannot import server/infra
Fix: move the server-side work behind an API route, or expose a shared
contract/schema under src/shared/contracts.
circular-dependency
Trigger: A.ts imports B.ts and B.ts imports A.ts (direct cycle
detection in guard/validator.ts:624-685).
Fix: extract the shared piece into a third module, or break the cycle with dependency inversion.
cross-slice
Trigger: same layer, different slice, direct import. For example a
widget in slice chart/ importing internals of slice table/.
Fix: import via the slice's public API (usually index.ts), or move the
shared piece to a layer both slices may import from.
deep-nesting
Trigger: import reaches into a slice past its public API (e.g.
@/client/widgets/chart/internal/helpers).
Fix: export the needed symbol from the slice's index.ts.
file-type
Trigger: file extension is .js or .jsx (source:
guard/validator.ts:233-237).
Guard FAIL src/client/utils/legacy.js:1
Rule: TypeScript Only
Fix: rename to .ts or .tsx and add types.
Fix: rename and type. No exceptions.
invalid-shared-segment
Trigger: a file under src/shared/ is in a segment that is not on the
allow-list (source: guard/validator.ts:155-188).
Allowed layout:
| Path | Purpose |
|---|---|
src/shared/contracts/ or src/shared/contracts.ts |
API contracts shared by server and client |
src/shared/schema/ or src/shared/schema.ts |
Zod schemas, DB schemas |
src/shared/types/ or src/shared/types.ts |
Pure TypeScript types |
src/shared/utils/client/ |
Utils safe for the client |
src/shared/utils/server/ |
Utils that require Node/Bun APIs |
src/shared/env/ or src/shared/env.ts |
Environment config (server-only readers) |
Anything else — src/shared/components/, src/shared/lib/, src/shared/helpers/ — is rejected.
Fix: pick the correct bucket. If it does not fit, it does not belong in shared.
FS-routes rules
Additional invariants apply only to files under app/** (source:
guard/validator.ts:392-498):
| File | Can import (if fsRoutes config present) |
|---|---|
app/**/page.tsx |
fsRoutes.pageCanImport layers only |
app/**/layout.tsx |
fsRoutes.layoutCanImport layers only |
app/**/route.ts |
fsRoutes.routeCanImport layers only |
And one hard rule regardless of config:
noPageToPage: apage.tsxcannot import anotherpage.tsx— not via relative path, not via alias, not via crawling throughapp/(source:guard/validator.ts:548-583).
Severity and configuration
Each violation type has a configurable severity (source:
guard/types.ts:34-47):
// mandu.config.ts
export default {
guard: {
severity: {
layerViolation: "error", // default
circularDependency: "warn", // default
deepNesting: "info", // default
crossSliceDependency: "warn", // default
fileType: "error", // default
invalidSharedSegment: "error", // default
},
},
};
"error" fails the build. "warn" surfaces in the reporter but does not
fail. "info" is logged only.
Error code → fix mapping
| Rule ID | Family | Canonical fix |
|---|---|---|
layer-violation |
imports | Add the target layer to the source layer's canImport, or route through an allowed layer |
circular-dependency |
imports | Extract shared code to a third module |
cross-slice |
imports | Import via the slice's public API |
deep-nesting |
imports | Export the symbol from index.ts |
file-type |
file | Rename .js/.jsx to .ts/.tsx |
invalid-shared-segment |
file | Move to an allowed src/shared/* bucket |
FORBIDDEN_IMPORT_IN_GENERATED |
codegen | Remove the forbidden fs/child_process/etc import from the generated file |
SLOT_MISSING_DEFAULT_EXPORT |
slots | Add export default Mandu.filling(...) |
SLOT_MISSING_FILLING_PATTERN |
slots | Wrap the handler in Mandu.filling() |
SLOT_ZOD_DIRECT_IMPORT |
slots | Import schemas from src/shared/schema, not zod directly |
ISLAND_FIRST_INTEGRITY |
island | Create a .island.tsx next to the page; do not import island() in page.tsx |
CLIENT_MODULE_NOT_FOUND |
island | The route's clientModule path does not exist — regenerate spec or add the file |
CONTRACT_NOT_FOUND |
contract | Create the contract file the spec points at |
CONTRACT_METHOD_NOT_IMPLEMENTED |
contract | Implement the missing method on the slot |
Anti-patterns
Do not do this.
- Disabling Guard to unblock a build. The violation is the signal. Fix the import or move the file — do not switch the severity down.
- Creating a new
src/shared/utils/bucket not on the allow-list. The allow-list is deliberately short. If you need another category, have a conversation about the architecture first.- Adding
"use client"to suppress an island-related error. The island contract cares about filename AND directive. Neither covers for the other.
Self-Healing Guard
Detecting a violation is half the job. checkWithHealing() + healAll()
close the loop — Guard analyzes each violation, proposes a fix, and
applies it when it's confident.
import { checkWithHealing, healAll, explainRule } from "@mandujs/core/guard";
const result = await checkWithHealing({ preset: "mandu" }, process.cwd());
if (result.items.length > 0) {
const healResult = await healAll(result);
console.log(`Fixed: ${healResult.fixed}, Failed: ${healResult.failed}`);
}
Fixable classes:
- layer-violation — re-route the import through
shared/or move the file to the right layer. - file-type — rename
foo.tsx→foo.island.tsxwhen the directive and filename disagree. - invalid-shared-segment — move the file under one of the allow-listed shared buckets.
- deep-nesting — flatten unnecessary directory levels.
Non-fixable (require decisions): circular dependencies spanning multiple modules, ambiguous cross-slice references, new invariants.
Explain any rule
Stuck on what a Guard rule means? Ask for an explanation:
const explanation = explainRule("layer-violation");
console.log(explanation.description);
console.log(explanation.examples);
console.log(explanation.fixStrategy);
Agents call this via mandu.guard.explain before attempting a heal —
understanding the rule prevents a patch that just relocates the violation.
MCP access
mandu.guard.check— report violations, no changesmandu.guard.analyze— per-violation diagnosismandu.guard.heal— propose fixes (diff, not applied)mandu.guard.explain— human/agent readable rule explanation
🤖 Agent Prompt
Apply the guidance from the Mandu docs page at https://mandujs.com/docs/architect/guard to my project.
Summary of the page:
Guard is a build-time static analyzer. It enforces six invariant families: layer-violation, circular-dependency, cross-slice, deep-nesting, file-type, invalid-shared-segment. It also runs FS-routes rules (page/layout/route import whitelists) and contract rules (slot/contract exports). Severity is configurable; 'error' fails the build. Each violation carries filePath, line, column, ruleName, ruleDescription, suggestions.
Required invariants — must hold after your changes:
- Any .js or .jsx file in a scanned layer is a file-type violation
- src/shared is restricted to {contracts, schema, types, utils/client, utils/server, env}; other segments are invalid-shared-segment
- Client-layer files cannot import shared/env (server-only)
- page.tsx cannot import another page.tsx (noPageToPage rule)
- Layer imports must appear in the importing layer's canImport list
- Cross-slice imports within the same layer are rejected unless via a public API
Then:
1. Make the change in my codebase consistent with the page.
2. Run `bun run guard` and `bun run check` to verify nothing
in src/ or app/ breaks Mandu's invariants.
3. Show me the diff and any guard violations.
For Agents
Guard is a build-time static analyzer. It enforces six invariant families: layer-violation, circular-dependency, cross-slice, deep-nesting, file-type, invalid-shared-segment. It also runs FS-routes rules (page/layout/route import whitelists) and contract rules (slot/contract exports). Severity is configurable; 'error' fails the build. Each violation carries filePath, line, column, ruleName, ruleDescription, suggestions.
- Any .js or .jsx file in a scanned layer is a file-type violation
- src/shared is restricted to {contracts, schema, types, utils/client, utils/server, env}; other segments are invalid-shared-segment
- Client-layer files cannot import shared/env (server-only)
- page.tsx cannot import another page.tsx (noPageToPage rule)
- Layer imports must appear in the importing layer's canImport list
- Cross-slice imports within the same layer are rejected unless via a public API