LangENKO

Guard

The invariant layer — every rule Mandu's static analyzer enforces, what each one means, and how to fix the violation.

since v0.22
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:

  1. shared/env is server-only. Any import from shared/env in a file whose layer starts with client/ is rejected with severity error (source: guard/validator.ts:277-287).
  2. 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: a page.tsx cannot import another page.tsx — not via relative path, not via alias, not via crawling through app/ (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.

  1. Disabling Guard to unblock a build. The violation is the signal. Fix the import or move the file — do not switch the severity down.
  2. 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.
  3. 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.tsxfoo.island.tsx when 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 changes
  • mandu.guard.analyze — per-violation diagnosis
  • mandu.guard.heal — propose fixes (diff, not applied)
  • mandu.guard.explain — human/agent readable rule explanation

🤖 Agent Prompt

🤖 Agent Prompt — Guard
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

AI hint

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.

Invariants
  • 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
Guard scope
guard