Examples prove rules
Markdown for agentsA rule that finds no problem looks the same as a rule that checks nothing. A typing error in a path makes a rule find no files, and the check then shows “no problems”. Examples prove that each rule can find what it is for.
Accepted and rejected
Section titled “Accepted and rejected”An example is a small set of files. A rule has two lists of them:
- Rejected: code that the rule must refuse. A rejected example proves that the rule can find a problem. Each rule that is not a table row must have one.
- Accepted: code that the rule must permit. An accepted example proves that the rule does not refuse correct code.
Write an example on one line, or as files with their text:
rule('no-react-in-shared', { files: 'shared', mayNotImport: { packages: ['react'] }, examples: { accepted: ['src/shared/price.ts imports src/shared/money.ts'], rejected: [{ 'src/shared/price.ts': "import { useState } from 'react';" }], },})The examples reference tells you how to write one line, and where archangel refuses one line.
What archangel test does
Section titled “What archangel test does”- Archangel reads the repository one time, as a check does.
- For each example, it lays the example’s files over the repository, in memory. A file of the example at the path of a repository file replaces that file, for that example only. A file of the example can import files of the repository.
- It checks only the files of the example, and their imports. It does not read the repository again, and it does not write a file to disk.
- It decides the result:
- A rejected example passes only when its own rule reports a problem that involves a file of the example. A problem from another rule, or from a repository file, does not count.
- An accepted example passes only when its own rule reports no problem in the example, and the rule looked at the example. A rule that never looked at it proves nothing.
- It fails each rule that has no rejected example, with the error
rule-unproven.
Three mistakes that examples find
Section titled “Three mistakes that examples find”This config has three mistakes. Each one looks correct when you read it:
import { defineConfig } from '@enhance-eng/archangel';
export default defineConfig({ parts: { feature: 'src/features/{feature}/**', shared: 'src/shared/**', }, rules: ({ rule }) => [ rule('feature-entry', { folders: 'src/features/*', mustContain: 'index.ts', examples: { accepted: ['src/shared/format.ts'], rejected: ['src/features/cart/model.ts'], }, }),
rule('no-react-in-shared', { files: 'shared', mayNotImport: { packages: ['react'] }, examples: { accepted: ['src/shared/format.ts imports src/shared/money.ts'], }, }), ],});$ npx archangel testfail feature-entry accepted #1 src/shared/format.ts "feature-entry" never looked at a folder of this example, so accepting it proves nothing; give the example a file in a folder the rule namesfail feature-entry rejected #1 src/features/cart/model.ts no rule rejected the example, and "feature-entry" should have; fix the rule or the examplepass no-react-in-shared accepted #1 src/shared/format.ts, src/shared/money.tserror[rule-unproven]: the rule "no-react-in-shared" has no rejected example, so nothing shows that it can find a violation; add one under examples.rejected | = note: rule: no-react-in-shared
3 examples, 2 failed; 1 rules without a rejected example- The accepted example of
feature-entryproves nothing. Its only file is insrc/shared, and the rule looks only at the folders insrc/features. The rule permits it because it never looked at it. - The rejected example of
feature-entryfails. The example putsmodel.tsinsrc/features/cart. The examples are laid over the repository, and the repository already hassrc/features/cart/index.ts. So the foldercarthas an index file, and the rule permits it. no-react-in-sharedhas no rejected example. Nothing shows that it can refuse an import.
Here is the fixed config. Each example uses a folder that the repository does not have, and each rule has a rejected example:
import { defineConfig } from '@enhance-eng/archangel';
export default defineConfig({ parts: { feature: 'src/features/{feature}/**', shared: 'src/shared/**', }, rules: ({ rule }) => [ rule('feature-entry', { folders: 'src/features/*', mustContain: 'index.ts', examples: { accepted: [{ 'src/features/search/index.ts': '', 'src/features/search/query.ts': '' }], rejected: ['src/features/search/query.ts'], }, }),
rule('no-react-in-shared', { files: 'shared', mayNotImport: { packages: ['react'] }, examples: { accepted: ['src/shared/format.ts imports src/shared/money.ts'], rejected: ['src/shared/format.ts imports react'], }, }), ],});$ npx archangel testpass feature-entry accepted #1 src/features/search/index.ts, src/features/search/query.tspass feature-entry rejected #1 src/features/search/query.tspass no-react-in-shared accepted #1 src/shared/format.ts, src/shared/money.tspass no-react-in-shared rejected #1 src/shared/format.ts4 examples, none failed; 0 rules without a rejected exampleTables and presets
Section titled “Tables and presets”- A table row needs no written example.
archangel rulesshows what each row judged, and a table that finds no file is the errortable-empty. You can write examples for a table. They run against all its rows, and a rejected example passes when one row refuses one of its imports. - A preset brings its own examples.
archangel testchecks them in your repository, under the preset’s root, with your tsconfig.
Run it in CI
Section titled “Run it in CI”Run archangel test next to archangel check. A change to the config can break an example, and a
change to the code can too, because examples are laid over the code. See
Use archangel in CI.