How archangel works
Markdown for agentsArchangel does two things. It makes a map of your code. Then each rule asks a question about the map. This page tells you how each step works, so you can reason about what a rule does.
1. The map of the code
Section titled “1. The map of the code”Archangel reads each source file below the config folder one time. For each file, the map holds:
- its path;
- its part, its instance and its groups;
- its imports, and the file or package that each import resolves to;
- the files that import it;
- what it exports.
Archangel reads the files that Git does not ignore, with the extensions .ts, .tsx, .mts,
.cts, .js, .jsx, .mjs and .cjs. It resolves each import as TypeScript does, with your
tsconfig paths and the exports of each package.json.
The map has no gaps. A file that archangel cannot parse is an error. An import that it cannot
resolve is an error, unless the config permits it with allowUnresolved. Archangel never skips a
file or an import without a word.
An import() of a template literal, such as import(`./locales/${code}.json`), is an import of
each file that it can load, as bundlers treat it: each ${…} is one file or folder name. It must
start with ./ or ../ and end in a written extension, as Vite asks. Archangel cannot follow any
other computed specifier, such as import(url), and the summary lists each one by file and line.
2. Names on the map
Section titled “2. Names on the map”The config gives names to sets of files:
- A part is a named area, such as
src/features/{feature}/**. Each file is in one part at most. - A capture, such as
{feature}, makes each folder one instance of the part. - A group is a named set of files that can cut across parts, such as the test files.
- An externals set is a named set of outside packages.
- In a table, a kind is a name that a pattern gives to a file, such as
entityinuser.entity.ts.
Read Concepts for each one.
3. Rules ask questions
Section titled “3. Rules ask questions”Each rule is a question about part of the map:
| Rule | The question |
|---|---|
| a table row | Does a file in this row import something that the row does not list? |
mayNotImport |
Does a file in these files import the other side? |
noCycles |
Do these files import each other in a cycle? |
mustBeNamed |
Does the name of each file match? |
| a plugin condition | Your own question, as a TypeScript function. |
When a rule decides to permit or refuse an import, it judges the import. An import that no rule
judges is not checked. archangel rules lists these imports, so you can see the gaps.
4. The loop
Section titled “4. The loop”You use three commands together:
-
archangel checkasks each rule its question, and shows each problem. This is the test of your code.Terminal $ npx archangel checkerror[may-import]: entity may not import event, through "./user-created.event": entity may import only value-object, and itself--> src/domain/user.entity.ts:2:29|2 | import { userCreated } from './user-created.event';| ^^^^^^^^^^^^^^^^^^^^^^ entity may import only value-object, and itself|= help: remove the import, or move the code to a file whose row permits the import= note: rule: model.entity= note: because: An entity does not know its events. A service publishes them.error[may-import]: routes may not import the package `pg`, through "pg": routes may import only services, and itself--> src/routes/health.ts:1:22|1 | import { Pool } from 'pg';| ^^^^ routes may import only services, and itself|= help: remove the import, or move the code to a file whose row permits the import= note: rule: backend.routes= note: because: A route calls a service. Only db talks to the database. The domain depends on nothing.checked 8 files and 10 imports: 2 problemsFor what a code means, run `archangel docs may-import`. -
archangel testchecks the examples of each rule. This is the test of your rules. A rule with a typing error in a path finds no file, so it cannot reject its example, and the test fails.Terminal $ npx archangel testpass no-cycles rejected #1 src/services/a.ts, src/services/b.ts1 examples, none failed; 0 rules without a rejected example7 table rows need no written example; `archangel rules` shows what each covers -
archangel rulesshows what each rule judged, and what no rule judges. This is how you see the reach of your tests.Terminal $ npx archangel rulesbackend 4 rows archangel.config.ts:14because: A route calls a service. Only db talks to the database. The domain depends on nothing.backend.routes: routes may import only services, and itself2 files · 2 imports judged · 1 problem · 0 exceptionsbackend.services: services may import only domain, db, and itself1 file · 2 imports judged · 0 problems · 0 exceptionsbackend.db: db may import only domain, driver, and itself1 file · 2 imports judged · 0 problems · 0 exceptionsbackend.domain: domain may import only itself3 files · 3 imports judged · 0 problems · 0 exceptionsbackend.every-file-has-a-kind: a file in no row is not checked here; add `files` to check it7 files · 0 problemsmodel 3 rows archangel.config.ts:23because: An entity does not know its events. A service publishes them.model.entity: entity may import only value-object, and itself1 file · 2 imports judged · 1 problem · 0 exceptionsmodel.event: event may import only entity, value-object, and itself1 file · 1 import judged · 0 problems · 0 exceptionsmodel.value-object: value-object may import only itself1 file · 0 imports judged · 0 problems · 0 exceptionsmodel.every-file-has-a-kind: every file of model has a row3 files · 0 problemsno-cycles archangel.config.ts:33because: Files that import each other cannot change one at a time.files in src/** may not import each other in a cycle7 files · 0 problemsNot judged by any rule: 0 files, 0 imports between repository files.Outside packages that no rule names: none.
When check passes, test passes, and rules shows no gap that you care about, your architecture
is tested.
The facts and the rules are separate
Section titled “The facts and the rules are separate”Which file imports which file is a fact about the code. Which imports are permitted is a decision in the config. Archangel keeps the two separate:
- A change to the config does not read the code again.
- A change to one file reads only that file again.
- An example of a rule is a small map in memory. So
archangel testchecks a rule with a few files, and writes no file.
Why it is fast
Section titled “Why it is fast”- A Rust core reads and parses each file one time, with oxc.
- The work of a check grows with the number of files and imports. The cost of one import does not grow with the number of rules.
- Archangel needs no type checker, because it checks only structure.
- In the editor and in watch mode, archangel keeps the map in memory. After a change, it reads only the changed files, and checks only what the change can affect.
On a 9,800-file monorepo, a full check of 400 rules takes 1.9 seconds on a laptop. The editor shows the result of a change in about 13 milliseconds.
--stats shows the work of a check as counts, such as the files read and the imports resolved. Read
Output and exit codes.
Where config code runs
Section titled “Where config code runs”A TypeScript config, and each plugin, runs in Node 22.18 or later. Archangel runs the config one time for each load. Plugins stay in that Node process, and archangel sends them only the facts that they read. A JSON config needs no Node.