Skip to content

Add archangel to an existing repository

Markdown for agents

This 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.

  • 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
  1. Install archangel and write a starter config:

    Terminal window
    npm install --save-dev @enhance-eng/archangel
    npx archangel init --typescript

    init makes one part for each folder of source files. Change these parts to the parts of your architecture.

  2. 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.',
    }),
    ],
    });
  3. Check the repository:

    Terminal
    $ npx archangel check
    error[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 problems
    For 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.

  1. Write the baseline:

    Terminal
    $ npx archangel baseline add
    wrote 3 entries to archangel.baseline.json

    Archangel writes archangel.baseline.json next 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.

  2. Check again:

    Terminal
    $ npx archangel check
    checked 6 files and 5 imports: no problems
    app.feature: 0 permitted by exceptions, 2 by the baseline
    app.shared: 0 permitted by exceptions, 1 by the baseline

    The check passes. The summary shows, for each rule, how many problems the baseline permits.

  3. Commit archangel.config.ts and archangel.baseline.json.

The baseline holds only the problems of today. Suppose that a new feature imports the cart:

src/features/billing/index.ts
import { cartTotal } from '../cart';
export const invoiceTotal = cartTotal;
Terminal
$ npx archangel check
error[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 baseline
For what a code means, run `archangel docs may-import`.

The new import fails the check. The three old problems stay permitted.

A check never writes the baseline. When the code fixes an old problem, the baseline entry becomes a problem of its own.

  1. Fix an old problem. Here, greeting.ts gets 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}`;
    }
  2. Check:

    Terminal
    $ npx archangel check
    error[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 problem
    app.feature: 0 permitted by exceptions, 2 by the baseline
    For what a code means, run `archangel docs baseline-stale`.

    The diagnostic baseline-stale tells you that the baseline permits more than the code needs.

  3. Remove the entries that the code does not need:

    Terminal
    $ npx archangel baseline prune
    wrote 2 entries to archangel.baseline.json

    baseline prune lowers 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
    }
    ]
    }
  4. Check again, and commit the smaller baseline:

    Terminal
    $ npx archangel check
    checked 7 files and 4 imports: no problems
    app.feature: 0 permitted by exceptions, 2 by the baseline

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.