Skip to content

Exceptions and baselines

Markdown for agents

This guide tells you how to let code break a rule for now, and keep each case visible. Archangel has two ways:

  • An exception is a comment above one import, with the rule and a reason.
  • The baseline is a file that records each problem that existed when a rule started.

Each check counts both, for each rule, in its summary. So a team can see the numbers go down. Neither one turns a rule off.

The example is a shop whose features must not import each other:

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.',
}),
],
});

An exception is a comment directly above the import: above the line of its specifier, or above the first line of its statement when the statement takes more than one line. It names the rule, and it gives the reason after a colon:

// archangel-allow app.feature: checkout shows the cart total until the total moves to shared
import { cartTotal } from '../cart';
  1. Look at the checkout feature. It imports two other features, and each import has an exception:

    src/features/checkout/index.ts
    // archangel-allow app.feature: checkout shows the cart total until the total moves to shared
    import { cartTotal } from '../cart';
    // archangel-allow app.feature
    import { currentUser } from '../account';
    export function checkoutSummary(prices: number[]): string {
    return `${currentUser()}: ${cartTotal(prices)}`;
    }
  2. Check:

    Terminal
    $ npx archangel check
    error[exception-without-reason]: the exception for "app.feature" gives no reason, so it permits nothing; write the reason after the colon
    --> src/features/checkout/index.ts:3:1
    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:4:29
    |
    4 | 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.
    checked 5 files and 4 imports: 2 problems
    app.feature: 1 permitted by exceptions, 0 by the baseline
    For what a code means, run `archangel docs exception-without-reason` or `archangel docs may-import`.

    The first exception permits its import. The second exception has no reason, so it permits nothing. Archangel shows exception-without-reason, and it also shows the import that the exception did not permit.

  3. Give the second exception a reason:

    src/features/checkout/index.ts
    // archangel-allow app.feature: checkout shows the cart total until the total moves to shared
    import { cartTotal } from '../cart';
    // archangel-allow app.feature: the account feature has no shared session yet
    import { currentUser } from '../account';
    export function checkoutSummary(prices: number[]): string {
    return `${currentUser()}: ${cartTotal(prices)}`;
    }
  4. Check again:

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

    The summary counts the two imports that exceptions permit.

An exception permits one import to break one rule. Archangel still checks the import against each other rule. To permit one import for two rules, write two comments on the lines directly above it. A blank line between a comment and the import breaks the link.

An exception must stay correct as the code changes.

The rule name must exist. A typing error in the name is exception-unknown-rule, and the error lists the names of the rules:

src/features/account/index.ts
// archangel-allow app.featur: the profile reads the cart size
import { cartTotal } from '../cart';
export function currentUser(): string {
return `Sam (${cartTotal([])})`;
}
Terminal
$ npx archangel check
error[exception-unknown-rule]: the exception names "app.featur", which is not a rule; the rules are app.feature, app.shared, app.every-file-has-a-kind
--> src/features/account/index.ts:1:1
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/account/index.ts:2:27
|
2 | 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 5 files and 5 imports: 2 problems
app.feature: 2 permitted by exceptions, 0 by the baseline
For what a code means, run `archangel docs exception-unknown-rule` or `archangel docs may-import`.

An exception must permit something. When the import below it no longer breaks the rule, the exception is exception-unused. So an exception goes away when the code is fixed:

src/features/cart/index.ts
// archangel-allow app.feature: the cart reads the user
import { formatMoney } from '../../shared/money';
export function cartTotal(prices: number[]): string {
return formatMoney(prices.reduce((sum, price) => sum + price, 0));
}
Terminal
$ npx archangel check
error[exception-unused]: the exception for "app.feature" permits nothing, because the next line does not break that rule; delete it
--> src/features/cart/index.ts:1:1
checked 5 files and 4 imports: 1 problem
app.feature: 2 permitted by exceptions, 0 by the baseline
For what a code means, run `archangel docs exception-unused`.

When you start a rule on code that already breaks it, run archangel baseline add. It writes each current problem to archangel.baseline.json. A check then fails only for new problems.

  • An entry records the rule, the importing file, the import specifier and a count. It does not record a line, so an edit above the import does not break the match.
  • A check never writes the baseline. When the code fixes a problem, its entry is a baseline-stale problem. archangel baseline prune lowers or removes the entry. It never adds one. So the baseline can only get smaller.
  • An entry for a rule that the config does not have is also baseline-stale. A baseline that is not valid JSON stops the check with baseline-invalid.

Add archangel to an existing repository shows the full cycle, from baseline add to baseline prune, with the output of each step.

You have Use
One import that breaks a rule on purpose, with a reason worth reading An exception
Many problems from before the rule existed The baseline
A rule that is wrong for part of the code Change the rule or the parts, not an exception
Files that archangel must not check, such as generated code The ignore key of the config
A rule that you do not want, from a preset The off key of the config

ignore takes lines in .gitignore syntax. A file that ignore removes is not in the check.