Exceptions and baselines
Markdown for agentsThis 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.
Before you start
Section titled “Before you start”- Read Getting started.
The example is a shop whose features must not import each other:
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.', }), ],});Write an exception
Section titled “Write an exception”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 sharedimport { cartTotal } from '../cart';-
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 sharedimport { cartTotal } from '../cart';// archangel-allow app.featureimport { currentUser } from '../account';export function checkoutSummary(prices: number[]): string {return `${currentUser()}: ${cartTotal(prices)}`;} -
Check:
Terminal $ npx archangel checkerror[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:1error[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 problemsapp.feature: 1 permitted by exceptions, 0 by the baselineFor 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. -
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 sharedimport { cartTotal } from '../cart';// archangel-allow app.feature: the account feature has no shared session yetimport { currentUser } from '../account';export function checkoutSummary(prices: number[]): string {return `${currentUser()}: ${cartTotal(prices)}`;} -
Check again:
Terminal $ npx archangel checkchecked 5 files and 4 imports: no problemsapp.feature: 2 permitted by exceptions, 0 by the baselineThe 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.
Archangel checks each exception
Section titled “Archangel checks each exception”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:
// archangel-allow app.featur: the profile reads the cart sizeimport { cartTotal } from '../cart';
export function currentUser(): string { return `Sam (${cartTotal([])})`;}$ npx archangel checkerror[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 baselineFor 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:
// archangel-allow app.feature: the cart reads the userimport { formatMoney } from '../../shared/money';
export function cartTotal(prices: number[]): string { return formatMoney(prices.reduce((sum, price) => sum + price, 0));}$ npx archangel checkerror[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 baselineFor what a code means, run `archangel docs exception-unused`.Use the baseline for old problems
Section titled “Use the baseline for old problems”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-staleproblem.archangel baseline prunelowers 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 withbaseline-invalid.
Add archangel to an existing repository shows the full
cycle, from baseline add to baseline prune, with the output of each step.
Which one to use
Section titled “Which one to use”| 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.
Next steps
Section titled “Next steps”- Rule options.
- Config file lists
ignoreandoff.