Skip to main content
Glama

faultline

faultline running live on withastro/astro: an edit that makes request handling import the dev server turns its edge red, a new allowed dependency shows up green, then the drill-down into Request handling

Recorded live on withastro/astro while real files changed. One edit crosses a fault line (production request handling importing the dev server), one adds an allowed dependency, and both are undone at the end. The systems and rules are an example map, not the Astro team's (examples/astro). Also: what Astro's production entry loads at startup.

faultline.anzalabidi.dev · A living architecture map for any codebase. You declare the systems once. Every change after that, yours or any coding agent's, shows up as light on a map that never moves, as a sentence in the PR, and as a short answer the agent can read before it writes the wrong import.

fault init        # propose systems from the repo, write faultline.yml
fault map         # open the live map; it redraws as files change
fault setup       # connect your agents: MCP, hooks, AGENTS.md, pre-commit
fault diff main   # what this branch did to the structure, in plain English
fault footprint   # what an entry point loads at startup, and where to cut it
fault check main  # exit 1 if it crosses a fault line or loosens a rule (CI)
fault sync        # place new folders, suggest rules learned from history
npm install -g @anzalabidi/faultline    # or run any command with npx -y @anzalabidi/faultline

Node 20 or newer. The command is fault.

It answers one question at a glance: what did this change do to the shape of the system? Not which lines moved. Which boxes started talking to each other, which boundaries got crossed, and the exact imports behind each of those.

✗ Crosses a fault line: Request handling → Dev server:
  packages/astro/src/core/app/origin-check.ts imports warnMissingAdapter from adapter-validation.ts.
  Allowed route: app → core-shared → dev-server
(~50 tokens)

Any language

JavaScript and TypeScript are parsed with oxc. Every other language goes through a small adapter: a comment- and string-aware lexer that reads the imports, plus a resolver that follows that language's own rules.

Language

Reads

Resolves through

TypeScript, JavaScript, Astro, Vue, Svelte

imports, re-exports, dynamic imports, require

relative paths, .js to .ts, index files, tsconfig paths, workspace exports

Python

import, from … import, TYPE_CHECKING blocks as type-only

package roots (__init__.py, src layouts), submodule-first from imports

Go

single and grouped imports

go.mod module paths to package directories

Rust

mod, nested use trees, extern crate, inline crate:: and other_crate:: paths; skips #[cfg(test)]

module tree (mod.rs, lib.rs), self, super, workspace crates from Cargo.toml

Java, Kotlin, Scala, Groovy

imports, wildcards, Scala {A, B}, Kotlin top-level functions

declared packages; same-package types by reference

C# (and .NET projects)

using, global using, namespaces, .csproj ProjectReference

types visible through the file's namespaces and usings

C, C++, Objective-C, CUDA

#include, #import

relative to the file, then the closest matching path

Ruby

require, require_relative, autoload, constants

Ruby's lexical constant lookup (Jekyll::Document sees Jekyll::X before ::X)

PHP

use (including groups), require, include

declared namespaces and class names

Swift

import, type references

SPM targets from Package.swift, declared types

Dart

import, export, part

package: names from pubspec.yaml, relative paths

Elixir

alias, import, use, require, module references

declared defmodule names

Lua, Haskell, Zig

require, import, @import

module paths and declared modules

Adding a language is one extractor and one resolver function; see src/lang/.

Exact and inferred edges

Some languages name files in their imports. Others (C#, Swift, same-package Java, Ruby constants) only name types. faultline tags every edge:

  • exact: the language's own import rules name the target file.

  • inferred: matched by a referenced type that the repo declares somewhere visible.

Rules only fire on exact edges. A wrong edge that blocks a commit is worse than a missing one, so an inferred reference can show on the map but never fails your build. Names that shadow platform types (String, File, View, Task, and so on) are skipped rather than guessed.

Measured accuracy

Checked against each language's own toolchain on public repos (bench/accuracy/, reproducible):

Repo

Checked against

Precision

Recall

pallets/flask

Python ast + PathFinder

100%

100%

psf/requests

Python ast + PathFinder

100%

100%

encode/httpx

Python ast + PathFinder

100%

100%

fastapi/fastapi

Python ast + PathFinder

100%

97.8% (the misses are test_*.py, ignored on purpose)

BurntSushi/ripgrep

cargo metadata, crate to crate

100%

100%

tokio-rs/axum

cargo metadata, crate to crate

100%

100%

tokio-rs/tokio

cargo metadata, crate to crate

100%

72.7% (the misses are bench and test crates, ignored on purpose)

Go, the JVM languages, C# and the rest were checked by hand on hugo, okhttp, spring-petclinic, eShop, redis, jekyll, laravel, swift-composable-architecture, cats, phoenix, telescope.nvim and zls. A native-toolchain comparison for them is the next benchmark to add.

Related MCP server: coderadius

Any agent

fault setup              # agents it detects in the repo
fault setup --agent all  # or: claude,cursor,codex,copilot,gemini,kiro,zed,opencode

Agent

MCP server

Told mid-turn when it crosses a fault line

Instructions

Claude Code

.mcp.json

PostToolUse hook

CLAUDE.md points to AGENTS.md

Cursor

.cursor/mcp.json

stop hook sends a follow-up message

AGENTS.md

OpenAI Codex

.codex/config.toml

PostToolUse hook (additionalContext)

AGENTS.md

GitHub Copilot (VS Code)

.vscode/mcp.json

PostToolUse hook (.github/hooks)

.github/copilot-instructions.md

Gemini CLI

.gemini/settings.json

GEMINI.md

Kiro

.kiro/settings/mcp.json

.kiro/steering/

Zed

.zed/settings.json

AGENTS.md

OpenCode

opencode.json

AGENTS.md

Windsurf, Cline

printed for their global config

AGENTS.md

Anything else

fault mcp over stdio, or the CLI

git pre-commit hook

AGENTS.md

fault setup also installs a git pre-commit hook (fault check --staged) that refuses a commit crossing a fault line, whoever wrote it. It is idempotent: run it twice and nothing changes.

Four tools, few tokens

The MCP server has four tools, each with a description under 40 words, because every connected agent pays for the tool list in every session.

Tool

Answers

map

the architecture: systems, what each owns, dependencies, fault lines, the plan

place

which system a path belongs to, what it must not import, and an allowed route when a direct import would cross a fault line

check

what your uncommitted work did to the structure, each finding with its evidence and a fix route

plan

declare a new dependency before writing it; the map and PR show planned versus actual

The same answers are on the CLI (fault overview, fault place, fault diff --format agent, fault plan) for agents without MCP. Every answer ends with its own cost, (~N tokens).

On Astro (967 source files, 22 systems), from node bench/tokens.mjs:

What the agent needs

With faultline

Without

Tool list, once per session

445

The whole architecture

1,225

11,838 just to list the paths; ~935,000 to read the files and see the imports

Where one new file goes and what it may import

70 to 150

What my edits did to the structure

5 to 200

Does it change what agents ship?

A pilot on the Astro monorepo: 36 headless Claude Code runs on tasks built to tempt one forbidden import. Where a rule covered the tempting import, agents crossed it 3 of 6 times with no guidance, 5 of 6 times with the rule written in AGENTS.md, and 0 of 6 times with faultline, at 46 to 62% higher average cost per run (the correct fix is a refactor). One repo, one model, tasks written by the builder: read it as a pilot. Method, every diff and the caveats are in bench/agent-ab.

The map

fault map serves a local page that watches the repo and redraws as files change:

  • Systems view. Every system with its dependencies. New edges are green, crossed fault lines red, removed edges red and dashed, planned edges dotted blue, and changed systems get an amber outline with file counts.

  • Drill in. Double-click a system to see its modules, grouped by the folder they come from, with callers on the left and dependencies on the right. Edges run between folders; select a module to see its own imports.

  • Footprint. Pick an entry point (a package export, a bin, a main.go or main.rs) and the map shows what it loads at startup: how much of each system, which npm packages, and the shortest import chain behind any file or package. Cut points are the files whose removal from the startup path drops the most with them. Open one directly with ?entry=<export or path>.

  • Evidence. Click an edge to see every import behind it, with the new ones highlighted.

  • Steer. Click an edge and choose Forbid this dependency to turn it into a rule in faultline.yml. Click a system and Plan a new dependency. Agents read both through faultline on their next call.

  • Timeline. Commits made during the session become steps, and so do agent turns. Scrub back through them, or compare each step to the one before.

The map is private to your machine: it binds to 127.0.0.1 and refuses writes from any other origin.

fault export and fault replay write the same UI to one HTML file you can share: a single change, or a stretch of history played back commit by commit.

faultline.yml

version: 1
systems:
  - id: web
    name: Web UI
    description: Pages and components.
    paths: [apps/web/src/**]
  - id: api
    name: API
    paths: [services/api/**]
  - id: db
    name: Database
    paths: [packages/db/**]
rules:
  - deny: web -> db
    reason: The UI talks to the API, never the database
  - deny: "{web,api} -> {scripts,tools}"
  • A file belongs to the most specific system whose glob matches it, so you can carve src/core/** out of a broader src/**.

  • Files outside every system show as Unmapped, and new ones are reported in each diff, so the map never drifts from the code without anyone noticing.

  • Rules take system ids or globs over ids. Type-only imports don't count unless the rule sets types: true.

  • fault init writes a first draft from the directory tree. fault init --outline prints the tree and the draft for whichever agent you use to name properly; fault init --ai asks Claude directly when ANTHROPIC_API_KEY is set.

Plan versus actual

fault plan "api -> billing: invoices need customer data"

Planned dependencies live in .faultline/plan.yml, next to the code. The map draws them dotted until the imports exist. The PR comment lists each one as built or not yet, and flags any new dependency between systems that nobody planned.

Keeping the map true

A map nobody updates starts lying, and a lying map gets ignored. faultline splits the upkeep in two, on purpose.

Where code lives is a fact, so it keeps itself current. A new folder outside every system shows up on the map with a suggestion: join the system its imports go to, or become a new one. fault sync --apply (or Place on the map) writes it. Agents are told to run it; placing code never loosens a rule.

$ fault sync
1 folder outside every system
  billing-lab/**  (2 files) → Core utilities  2 of its 3 imports to and from other systems involve Core utilities

Suggested rules (a person decides these; add one with fault sync --rule "a -> b")
  deny core-shared -> cli  CLI depends on Core utilities (38 imports) and Core utilities has not imported CLI in the last 200 commits. Keeps it one-way.

What may depend on what is a decision, so only people make it. Rules are suggested from history: if A imports B and B has never imported A in the last 200 commits, faultline offers deny B -> A to keep it one-way. You add it with one click or fault sync --rule. Nothing adds a rule on its own.

Loosening needs a human. An agent that hits a red line could simply delete it. So:

  • The Claude Code hook refuses any edit that removes or narrows a rule, moves files out of a system, ignores mapped files, or leaves faultline.yml invalid. Other agents are told right after the edit.

  • fault check judges a change against the stricter of the two faultline.yml versions, and fails when the file gets looser. Approve it with --allow-loosening or FAULTLINE_ALLOW_LOOSENING=1.

  • The pull request comment says so first, and the GitHub Action fails unless allow-loosening is set, for example from a label:

      - uses: anzal1/faultline@v0
        with:
          allow-loosening: ${{ contains(github.event.pull_request.labels.*.name, 'faultline-approved') }}

In CI

# .github/workflows/faultline.yml
on: pull_request
permissions:
  contents: read
  pull-requests: write
jobs:
  faultline:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
        with: { fetch-depth: 0 }
      - uses: anzal1/faultline@v0

The action runs from its own source, so it needs no package registry. Every pull request gets one sticky comment: the headline, each structural change as a sentence, a Mermaid map of only the part that changed (GitHub renders it inline), plan versus actual, and the imports behind each change. The check fails when the PR crosses a fault line or loosens faultline.yml without approval. On GitLab, Bitbucket or anything else, run fault diff "$BASE" "$HEAD" --format markdown and post the output.

How it works

  1. List files. A snapshot is a git ref (read straight from the object store, no checkout), the staged index, or the working tree.

  2. Parse. Each file goes to its language's extractor. Results are cached by git blob hash, so after the first run a snapshot of a large repo rebuilds in a fraction of a second.

  3. Resolve. Each import is resolved by its language's rules, and every edge is tagged exact or inferred.

  4. Assign. Each file goes to a system and a module (the first folder under the system's root, named after its folder when a system spans several).

  5. Aggregate and diff. File edges roll up into module and system edges, and two snapshots are compared.

Every repo named above builds its full graph in under a second on a laptop, cold, with no cache. Replaying the last 260 commits of the Astro monorepo takes about 30 seconds.

Footprint

$ fault footprint astro/app/entrypoint/prod --why zod
astro/app/entrypoint/prod  packages/astro/src/core/app/entrypoints/virtual/prod.ts
Loads 159 files at startup across 11 systems.
  Runtime                      37 of 76
  Routing                      35 of 41
  Request handling             30 of 59
  ...
npm at startup: @oslojs/encoding, clsx, cookie, devalue, html-escaper, piccolore, unstorage, zod

Cut points: stop importing the file and this many files stop loading at startup
    11  astro/src/core/routing/handler.ts
     ...
     4  astro/src/core/session/provider.ts  drops unstorage

Why zod loads at startup
  ... > core/fetch/fetch-state.ts {FetchState} > core/encryption.ts {generateCspDigest} > core/csp/config.ts {ALGORITHMS, CspHash}

Startup means static, non-type imports reachable from the entry; files reached only through import() count as on demand. Cut points come from the dominator tree of that graph: every startup path to a file passes through its dominators, so no longer importing one drops its whole subtree. It counts files and packages, not bytes or milliseconds, and a bundler may still tree-shake some of what it lists.

Limits

  • Imports built from runtime strings (import(\./locale/${lang}`)`) are not followed.

  • Edges are code dependencies. Calls over HTTP, queues or a DI container are not on the map yet.

  • Inferred edges come from type names. They are good enough to draw and never enforced.

Development

npm install
npm run build
npm test
node dist/cli.js -C path/to/repo map

MIT licensed.

Related MCP Connectors

Related MCP Servers

  • A
    license
    Not graded
    quality
    C
    maintenance
    Enables AI agents to query architecture context, data contracts, and blast radius to prevent cross-repo architectural breakage before merging.
    24
    Apache 2.0
  • A
    license
    Not graded
    quality
    A
    maintenance
    Provides a dependency graph of any local repository with tools for change impact, transitive dependents, health audits, and more, enabling AI coding agents to see structure and refactor safely.
    4,912 npm
    4
    MIT
  • A
    license
    A
    quality
    C
    maintenance
    Extracts deterministic architecture maps from codebases for AI agents, enabling queries about blast radius, routes, security findings, and production readiness without sending code anywhere.
    6
    45 PyPI
    MIT