Add archangel to an existing repository
Markdown for agentsThis guide turns on an architecture rule in a repository that already breaks it. The check then fails only for new problems, and the old problems can only go down.
Before you start
Section titled “Before you start”- You need Node 22.18 or later.
- Read Getting started if you do not know what a table is.
The example repository is a shop with three features and some shared code. Some of its code already breaks the decision “a feature does not import another feature”:
- package.json
Directorysrc/
Directoryfeatures/
Directoryaccount/
- index.ts
Directorycart/
- index.ts
Directorycheckout/
- index.ts
Directoryshared/
- greeting.ts
- money.ts
Write the rule
Section titled “Write the rule”-
Install archangel and write a starter config:
Terminal window npm install --save-dev @enhance-eng/archangelnpx archangel init --typescriptinitmakes one part for each folder of source files. Change these parts to the parts of your architecture. -
Write the decision as a table:
archangel.config.ts import { defineConfig } from '@enhance-eng/archangel';export default defineConfig({parts: {feature: 'src/features/{feature}/**',shared: 'src/shared/**',},rules: ({ mayImport }) => [mayImport('app', {feature: ['shared'],shared: [],}, {because: 'A feature changes alone. Code that two features need goes in shared.',}),],}); -
Check the repository:
Terminal $ npx archangel checkerror[may-import]: feature may not import another instance of feature, through "../cart": feature may import only shared, and itself in the same instance--> src/features/checkout/index.ts:1:27|1 | import { cartTotal } from '../cart';| ^^^^^^^^^ feature may import only shared, and itself in the same instance|= help: remove the import, or move the code to a file whose row permits the import= note: rule: app.feature= note: because: A feature changes alone. Code that two features need goes in shared.error[may-import]: feature may not import another instance of feature, through "../account": feature may import only shared, and itself in the same instance--> src/features/checkout/index.ts:2:29|2 | import { currentUser } from '../account';| ^^^^^^^^^^^^ feature may import only shared, and itself in the same instance|= help: remove the import, or move the code to a file whose row permits the import= note: rule: app.feature= note: because: A feature changes alone. Code that two features need goes in shared.error[may-import]: shared may not import feature, through "../features/account": shared may import only itself--> src/shared/greeting.ts:1:29|1 | import { currentUser } from '../features/account';| ^^^^^^^^^^^^^^^^^^^^^ shared may import only itself|= help: remove the import, or move the code to a file whose row permits the import= note: rule: app.shared= note: because: A feature changes alone. Code that two features need goes in shared.checked 6 files and 5 imports: 3 problemsFor what a code means, run `archangel docs may-import`.The check finds three problems. Each one existed before the rule. The check exits with code 1, so CI fails.
Now you have two choices. You can fix all the problems before you merge the rule. Or you can record them, merge the rule today, and fix them later. In a large repository, the second choice is usually the only one that is possible.
Record the old problems in a baseline
Section titled “Record the old problems in a baseline”-
Write the baseline:
Terminal $ npx archangel baseline addwrote 3 entries to archangel.baseline.jsonArchangel writes
archangel.baseline.jsonnext to the config:archangel.baseline.json {"entries": [{"rule": "app.feature","file": "src/features/checkout/index.ts","specifier": "../account","count": 1},{"rule": "app.feature","file": "src/features/checkout/index.ts","specifier": "../cart","count": 1},{"rule": "app.shared","file": "src/shared/greeting.ts","specifier": "../features/account","count": 1}]}An entry records the rule, the file, the import specifier and a count. It does not record a line number, so an edit above the import does not break the match. The file is sorted, so two runs on the same code write the same bytes.
-
Check again:
Terminal $ npx archangel checkchecked 6 files and 5 imports: no problemsapp.feature: 0 permitted by exceptions, 2 by the baselineapp.shared: 0 permitted by exceptions, 1 by the baselineThe check passes. The summary shows, for each rule, how many problems the baseline permits.
-
Commit
archangel.config.tsandarchangel.baseline.json.
A new problem still fails
Section titled “A new problem still fails”The baseline holds only the problems of today. Suppose that a new feature imports the cart:
import { cartTotal } from '../cart';
export const invoiceTotal = cartTotal;$ npx archangel checkerror[may-import]: feature may not import another instance of feature, through "../cart": feature may import only shared, and itself in the same instance --> src/features/billing/index.ts:1:27 |1 | import { cartTotal } from '../cart'; | ^^^^^^^^^ feature may import only shared, and itself in the same instance | = help: remove the import, or move the code to a file whose row permits the import = note: rule: app.feature = note: because: A feature changes alone. Code that two features need goes in shared.
checked 7 files and 6 imports: 1 problem app.feature: 0 permitted by exceptions, 2 by the baseline app.shared: 0 permitted by exceptions, 1 by the baselineFor what a code means, run `archangel docs may-import`.The new import fails the check. The three old problems stay permitted.
The baseline only gets smaller
Section titled “The baseline only gets smaller”A check never writes the baseline. When the code fixes an old problem, the baseline entry becomes a problem of its own.
-
Fix an old problem. Here,
greeting.tsgets the name as an argument, so it does not import the account feature:src/shared/greeting.ts export function greeting(name: string): string {return `Hello, ${name}`;} -
Check:
Terminal $ npx archangel checkerror[baseline-stale]: the baseline permits 1 more violation(s) of "app.shared" in src/shared/greeting.ts than the code has, because they were fixed; run `archangel baseline prune`checked 7 files and 4 imports: 1 problemapp.feature: 0 permitted by exceptions, 2 by the baselineFor what a code means, run `archangel docs baseline-stale`.The diagnostic
baseline-staletells you that the baseline permits more than the code needs. -
Remove the entries that the code does not need:
Terminal $ npx archangel baseline prunewrote 2 entries to archangel.baseline.jsonbaseline prunelowers or removes entries. It never adds an entry, and it never raises a count.archangel.baseline.json {"entries": [{"rule": "app.feature","file": "src/features/checkout/index.ts","specifier": "../account","count": 1},{"rule": "app.feature","file": "src/features/checkout/index.ts","specifier": "../cart","count": 1}]} -
Check again, and commit the smaller baseline:
Terminal $ npx archangel checkchecked 7 files and 4 imports: no problemsapp.feature: 0 permitted by exceptions, 2 by the baseline
Find what no rule checks
Section titled “Find what no rule checks”A new rule can check less than you think. Run npx archangel rules after you add a rule. At the
end, it lists the files and imports that no rule judges, and the outside packages that no rule
names. Add a part, a row or a rule for each item that matters.
Next steps
Section titled “Next steps”- Exceptions and baselines tells you when to use an exception comment instead of the baseline.
- Use archangel in CI.
- Layer a backend shows a larger table.