@lucasacoutinho/archer 0.2.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (214) hide show
  1. package/README.md +145 -387
  2. package/dist/analyzer/Analyzer.d.ts +17 -0
  3. package/dist/analyzer/Analyzer.d.ts.map +1 -0
  4. package/dist/analyzer/Analyzer.js +15 -0
  5. package/dist/analyzer/Analyzer.js.map +1 -0
  6. package/dist/analyzer/bindings.d.ts +26 -0
  7. package/dist/analyzer/bindings.d.ts.map +1 -0
  8. package/dist/analyzer/bindings.js +61 -0
  9. package/dist/analyzer/bindings.js.map +1 -0
  10. package/dist/analyzer/errors.d.ts +34 -0
  11. package/dist/analyzer/errors.d.ts.map +1 -0
  12. package/dist/analyzer/errors.js +20 -0
  13. package/dist/analyzer/errors.js.map +1 -0
  14. package/dist/analyzer/graph-file.d.ts +13 -0
  15. package/dist/analyzer/graph-file.d.ts.map +1 -0
  16. package/dist/analyzer/graph-file.js +22 -0
  17. package/dist/analyzer/graph-file.js.map +1 -0
  18. package/dist/analyzer/paths.d.ts +12 -0
  19. package/dist/analyzer/paths.d.ts.map +1 -0
  20. package/dist/analyzer/paths.js +26 -0
  21. package/dist/analyzer/paths.js.map +1 -0
  22. package/dist/analyzer/server-exit.d.ts +19 -0
  23. package/dist/analyzer/server-exit.d.ts.map +1 -0
  24. package/dist/analyzer/server-exit.js +70 -0
  25. package/dist/analyzer/server-exit.js.map +1 -0
  26. package/dist/analyzer/source.d.ts +29 -0
  27. package/dist/analyzer/source.d.ts.map +1 -0
  28. package/dist/analyzer/source.js +58 -0
  29. package/dist/analyzer/source.js.map +1 -0
  30. package/dist/analyzer/syntax.d.ts +29 -0
  31. package/dist/analyzer/syntax.d.ts.map +1 -0
  32. package/dist/analyzer/syntax.js +226 -0
  33. package/dist/analyzer/syntax.js.map +1 -0
  34. package/dist/analyzer/targets.d.ts +26 -0
  35. package/dist/analyzer/targets.d.ts.map +1 -0
  36. package/dist/analyzer/targets.js +84 -0
  37. package/dist/analyzer/targets.js.map +1 -0
  38. package/dist/analyzer/typescript.d.ts +5 -0
  39. package/dist/analyzer/typescript.d.ts.map +1 -0
  40. package/dist/analyzer/typescript.js +221 -0
  41. package/dist/analyzer/typescript.js.map +1 -0
  42. package/dist/analyzer/workspace.d.ts +24 -0
  43. package/dist/analyzer/workspace.d.ts.map +1 -0
  44. package/dist/analyzer/workspace.js +9 -0
  45. package/dist/analyzer/workspace.js.map +1 -0
  46. package/dist/cli/explain.d.ts +3 -0
  47. package/dist/cli/explain.d.ts.map +1 -0
  48. package/dist/cli/explain.js +21 -0
  49. package/dist/cli/explain.js.map +1 -0
  50. package/dist/cli/main.d.ts +3 -0
  51. package/dist/cli/main.d.ts.map +1 -0
  52. package/dist/cli/main.js +159 -0
  53. package/dist/cli/main.js.map +1 -0
  54. package/dist/config/input.d.ts +59 -0
  55. package/dist/config/input.d.ts.map +1 -0
  56. package/dist/config/input.js +115 -0
  57. package/dist/config/input.js.map +1 -0
  58. package/dist/config/rules.d.ts +94 -0
  59. package/dist/config/rules.d.ts.map +1 -0
  60. package/dist/config/rules.js +130 -0
  61. package/dist/config/rules.js.map +1 -0
  62. package/dist/core/baseline.d.ts +16 -0
  63. package/dist/core/baseline.d.ts.map +1 -0
  64. package/dist/core/baseline.js +25 -0
  65. package/dist/core/baseline.js.map +1 -0
  66. package/dist/core/check.d.ts +16 -0
  67. package/dist/core/check.d.ts.map +1 -0
  68. package/dist/core/check.js +271 -0
  69. package/dist/core/check.js.map +1 -0
  70. package/dist/core/config.d.ts +72 -0
  71. package/dist/core/config.d.ts.map +1 -0
  72. package/dist/core/config.js +78 -0
  73. package/dist/core/config.js.map +1 -0
  74. package/dist/core/fingerprint.d.ts +54 -0
  75. package/dist/core/fingerprint.d.ts.map +1 -0
  76. package/dist/core/fingerprint.js +182 -0
  77. package/dist/core/fingerprint.js.map +1 -0
  78. package/dist/core/keys.d.ts +10 -0
  79. package/dist/core/keys.d.ts.map +1 -0
  80. package/dist/core/keys.js +16 -0
  81. package/dist/core/keys.js.map +1 -0
  82. package/dist/core/lower.d.ts +13 -0
  83. package/dist/core/lower.d.ts.map +1 -0
  84. package/dist/core/lower.js +82 -0
  85. package/dist/core/lower.js.map +1 -0
  86. package/dist/core/model.d.ts +155 -0
  87. package/dist/core/model.d.ts.map +1 -0
  88. package/dist/core/model.js +116 -0
  89. package/dist/core/model.js.map +1 -0
  90. package/dist/core/pattern.d.ts +13 -0
  91. package/dist/core/pattern.d.ts.map +1 -0
  92. package/dist/core/pattern.js +29 -0
  93. package/dist/core/pattern.js.map +1 -0
  94. package/dist/core/reach.d.ts +10 -0
  95. package/dist/core/reach.d.ts.map +1 -0
  96. package/dist/core/reach.js +81 -0
  97. package/dist/core/reach.js.map +1 -0
  98. package/dist/core/rule.d.ts +200 -0
  99. package/dist/core/rule.d.ts.map +1 -0
  100. package/dist/core/rule.js +162 -0
  101. package/dist/core/rule.js.map +1 -0
  102. package/dist/core/selector.d.ts +7 -0
  103. package/dist/core/selector.d.ts.map +1 -0
  104. package/dist/core/selector.js +27 -0
  105. package/dist/core/selector.js.map +1 -0
  106. package/dist/core/structural.d.ts +10 -0
  107. package/dist/core/structural.d.ts.map +1 -0
  108. package/dist/core/structural.js +24 -0
  109. package/dist/core/structural.js.map +1 -0
  110. package/dist/core/text.d.ts +26 -0
  111. package/dist/core/text.d.ts.map +1 -0
  112. package/dist/core/text.js +144 -0
  113. package/dist/core/text.js.map +1 -0
  114. package/dist/effect.d.ts +19 -0
  115. package/dist/effect.d.ts.map +1 -0
  116. package/dist/effect.js +21 -0
  117. package/dist/effect.js.map +1 -0
  118. package/dist/index.d.ts +24 -44
  119. package/dist/index.d.ts.map +1 -0
  120. package/dist/index.js +59 -52
  121. package/dist/index.js.map +1 -0
  122. package/dist/report/agent.d.ts +3 -0
  123. package/dist/report/agent.d.ts.map +1 -0
  124. package/dist/report/agent.js +35 -0
  125. package/dist/report/agent.js.map +1 -0
  126. package/dist/report/dot.d.ts +4 -0
  127. package/dist/report/dot.d.ts.map +1 -0
  128. package/dist/report/dot.js +15 -0
  129. package/dist/report/dot.js.map +1 -0
  130. package/dist/report/github.d.ts +3 -0
  131. package/dist/report/github.d.ts.map +1 -0
  132. package/dist/report/github.js +18 -0
  133. package/dist/report/github.js.map +1 -0
  134. package/dist/report/json.d.ts +3 -0
  135. package/dist/report/json.d.ts.map +1 -0
  136. package/dist/report/json.js +4 -0
  137. package/dist/report/json.js.map +1 -0
  138. package/dist/report/message.d.ts +20 -0
  139. package/dist/report/message.d.ts.map +1 -0
  140. package/dist/report/message.js +73 -0
  141. package/dist/report/message.js.map +1 -0
  142. package/dist/report/pretty.d.ts +8 -0
  143. package/dist/report/pretty.d.ts.map +1 -0
  144. package/dist/report/pretty.js +89 -0
  145. package/dist/report/pretty.js.map +1 -0
  146. package/dist/report/render.d.ts +9 -0
  147. package/dist/report/render.d.ts.map +1 -0
  148. package/dist/report/render.js +10 -0
  149. package/dist/report/render.js.map +1 -0
  150. package/dist/report/report.d.ts +44 -0
  151. package/dist/report/report.d.ts.map +1 -0
  152. package/dist/report/report.js +49 -0
  153. package/dist/report/report.js.map +1 -0
  154. package/dist/report/sarif.d.ts +3 -0
  155. package/dist/report/sarif.d.ts.map +1 -0
  156. package/dist/report/sarif.js +68 -0
  157. package/dist/report/sarif.js.map +1 -0
  158. package/dist/run.d.ts +49 -0
  159. package/dist/run.d.ts.map +1 -0
  160. package/dist/run.js +87 -0
  161. package/dist/run.js.map +1 -0
  162. package/dist/shell/baseline-file.d.ts +20 -0
  163. package/dist/shell/baseline-file.d.ts.map +1 -0
  164. package/dist/shell/baseline-file.js +46 -0
  165. package/dist/shell/baseline-file.js.map +1 -0
  166. package/dist/shell/load-config.d.ts +19 -0
  167. package/dist/shell/load-config.d.ts.map +1 -0
  168. package/dist/shell/load-config.js +39 -0
  169. package/dist/shell/load-config.js.map +1 -0
  170. package/package.json +29 -177
  171. package/skills/archer/SKILL.md +127 -45
  172. package/skills/archer/agents/openai.yaml +2 -2
  173. package/skills/archer/references/rules.md +267 -54
  174. package/dist/bun/plugin.cjs +0 -85
  175. package/dist/bun/plugin.d.cts +0 -7
  176. package/dist/bun/plugin.d.ts +0 -7
  177. package/dist/bun/plugin.js +0 -11
  178. package/dist/bun.cjs +0 -2238
  179. package/dist/bun.d.cts +0 -13
  180. package/dist/bun.d.ts +0 -13
  181. package/dist/bun.js +0 -28
  182. package/dist/chunk-2AKMBLHG.js +0 -2165
  183. package/dist/chunk-67YEOTXP.js +0 -25
  184. package/dist/chunk-IDVLHNL2.js +0 -46
  185. package/dist/chunk-ZZ6TR4PO.js +0 -201
  186. package/dist/cli.js +0 -2643
  187. package/dist/index.cjs +0 -2484
  188. package/dist/index.d.cts +0 -45
  189. package/dist/jest/plugin.cjs +0 -85
  190. package/dist/jest/plugin.d.cts +0 -7
  191. package/dist/jest/plugin.d.ts +0 -7
  192. package/dist/jest/plugin.js +0 -11
  193. package/dist/jest.cjs +0 -2237
  194. package/dist/jest.d.cts +0 -13
  195. package/dist/jest.d.ts +0 -13
  196. package/dist/jest.js +0 -27
  197. package/dist/matchers-Cqnbsyxe.d.cts +0 -11
  198. package/dist/matchers-Cqnbsyxe.d.ts +0 -11
  199. package/dist/node.cjs +0 -2237
  200. package/dist/node.d.cts +0 -13
  201. package/dist/node.d.ts +0 -13
  202. package/dist/node.js +0 -27
  203. package/dist/reporters-BbAXMpuW.d.cts +0 -6
  204. package/dist/reporters-xh-HQneS.d.ts +0 -6
  205. package/dist/suite-BwnPDCvU.d.cts +0 -337
  206. package/dist/suite-BwnPDCvU.d.ts +0 -337
  207. package/dist/vitest/plugin.cjs +0 -85
  208. package/dist/vitest/plugin.d.cts +0 -7
  209. package/dist/vitest/plugin.d.ts +0 -7
  210. package/dist/vitest/plugin.js +0 -11
  211. package/dist/vitest.cjs +0 -2531
  212. package/dist/vitest.d.cts +0 -37
  213. package/dist/vitest.d.ts +0 -37
  214. package/dist/vitest.js +0 -139
package/README.md CHANGED
@@ -1,459 +1,217 @@
1
1
  # Archer
2
2
 
3
- Architecture tests for TypeScript and JavaScript. Archer builds a resolved
4
- module graph, lets you describe boundaries with a fluent API, and reports a
5
- normal test failure when the code stops matching the design.
6
-
7
- It takes its readable rule style from [Pest architecture testing](https://pestphp.com/docs/arch-testing),
8
- but models files, packages, imports, exports, and TypeScript declarations.
9
-
10
- ## Install
3
+ Architecture tests for TypeScript and JavaScript. You describe which parts of
4
+ your code may depend on which. Archer analyzes the project with TypeScript and
5
+ reports every import that breaks the rules.
11
6
 
12
7
  ```sh
13
- npm install --save-dev @lucasacoutinho/archer typescript
8
+ npm install --save-dev @lucasacoutinho/archer
14
9
  ```
15
10
 
16
- Archer supports Node.js 20 and newer and TypeScript 5.7 through 6.x. The core
17
- package has no test-runner dependency.
18
-
19
- ## Start with the CLI
20
-
21
- Create `archer.config.ts`:
22
-
23
- ```ts
24
- import { defineConfig } from '@lucasacoutinho/archer'
25
-
26
- export default defineConfig({
27
- include: ['src/**/*.ts', 'packages/*/src/**/*.ts'],
28
- exclude: ['**/*.generated.ts'],
11
+ Archer needs Node 22.18 or newer and is ESM only.
29
12
 
30
- rules(archer) {
31
- archer
32
- .expect('src/domain/**')
33
- .not.toDependOn('src/infrastructure/**')
34
- .because('The domain must remain portable.')
35
- .as('boundaries/domain-isolation')
36
-
37
- archer.expect('src/**').toBeFreeOfCycles()
38
- },
39
- })
40
- ```
41
-
42
- Then run:
13
+ ## Quick start
43
14
 
44
15
  ```sh
45
- npx archer check
46
- npx archer check --watch
47
- ```
48
-
49
- `archer init` creates a starter config. Config paths, output paths, and the
50
- project root are resolved relative to the config file.
51
-
52
- ## Test-runner adapters
53
-
54
- Each adapter registers a test immediately and returns the same rule builder.
55
-
56
- ```ts
57
- // test/architecture.test.ts
58
- import { arch } from '@lucasacoutinho/archer/vitest'
59
-
60
- arch('the domain stays independent')
61
- .expect('src/domain/**')
62
- .not.toDependOn('src/infrastructure/**')
63
- .and.toOnlyDependOn(['src/domain/**', 'src/shared/**'])
64
- ```
65
-
66
- The other entry points are:
67
-
68
- ```ts
69
- import { arch } from '@lucasacoutinho/archer/jest'
70
- import { arch } from '@lucasacoutinho/archer/bun'
71
- import { arch } from '@lucasacoutinho/archer/node'
16
+ npx archer init # writes archer.config.ts
17
+ npx archer check # exit 0: pass, 1: violations, 2: cannot check
72
18
  ```
73
19
 
74
- Jest and Bun accept `timeout` as an Archer adapter option. The Node adapter
75
- accepts built-in `node:test` options under `test`.
76
-
77
- ## Vitest config plugin
78
-
79
- Vitest can run Archer once, before it starts collecting tests. The plugin loads
80
- `archer.config.*` from the Vitest project root by default:
81
-
82
20
  ```ts
83
- // vitest.config.ts
84
- import { archer } from '@lucasacoutinho/archer/vitest'
85
- import { defineConfig } from 'vitest/config'
21
+ // archer.config.ts
22
+ import { defineConfig, presets, r } from "@lucasacoutinho/archer"
86
23
 
87
24
  export default defineConfig({
88
- plugins: [archer()],
89
- })
90
- ```
91
-
92
- Pass `config`, `reporter`, `baseline`, `output`, or `failOnWarnings` to
93
- `archer()` when the defaults are not enough. This is a configuration plugin,
94
- so it performs one architecture check for the Vitest project instead of
95
- registering a test in every worker.
96
-
97
- ## Matcher setup modules
98
-
99
- Vitest, Jest, and Bun can also install Archer as an `expect` plugin. This keeps
100
- the runner integration in its normal setup hook and lets architecture tests use
101
- the same assertion in every runner.
102
-
103
- For Vitest, add the plugin to `setupFiles`:
104
-
105
- ```ts
106
- // vitest.config.ts
107
- import { defineConfig } from 'vitest/config'
108
-
109
- export default defineConfig({
110
- test: {
111
- setupFiles: ['@lucasacoutinho/archer/vitest/plugin'],
25
+ layers: {
26
+ app: "src/app",
27
+ domain: "src/domain",
28
+ infrastructure: "src/infrastructure",
112
29
  },
30
+ // Top to bottom: a layer may depend only on the layers below it.
31
+ stack: ["app", "domain"],
32
+ // Further dependencies, by layer.
33
+ allow: { infrastructure: ["domain"] },
34
+ rules: [
35
+ ...presets.recommended, // no import cycles, every import resolves
36
+ r.forbid("domain/pure", "layer:domain", ["npm:*", "node:*"]),
37
+ ],
113
38
  })
114
39
  ```
115
40
 
116
- For Jest, add it to `setupFilesAfterEnv`:
117
-
118
- ```ts
119
- // jest.config.ts
120
- export default {
121
- setupFilesAfterEnv: ['@lucasacoutinho/archer/jest/plugin'],
122
- }
123
- ```
124
-
125
- For Bun, preload it from `bunfig.toml`:
126
-
127
- ```toml
128
- [test]
129
- preload = ["@lucasacoutinho/archer/bun/plugin"]
130
- ```
131
-
132
- Then create a normal test and await the asynchronous matcher:
133
-
134
- ```ts
135
- import { createArcher } from '@lucasacoutinho/archer'
136
-
137
- test('the architecture is respected', async () => {
138
- const archer = createArcher()
139
- archer.expect('src/domain/**').not.toDependOn('src/infrastructure/**')
140
-
141
- await expect(archer).toRespectArchitecture()
142
- })
143
- ```
144
-
145
- The plugin installs the matcher; it does not silently inject a test into every
146
- worker. Use the `arch()` adapter when the fluent one-call form is preferable.
147
-
148
- ## Dependency rules
41
+ To run the same check in your test suite, add one test. This works in Vitest,
42
+ `node:test`, Bun, and Jest in ESM mode:
149
43
 
150
44
  ```ts
151
- archer
152
- .expect('src/application/**')
153
- .toDependOn('src/domain/**')
154
- .not.toDependOn('src/web/**')
155
- .toOnlyDependOn(['src/application/**', 'src/domain/**', 'npm:zod'])
156
- .toBeDependedOnBy('src/http/**')
157
- .toOnlyBeDependedOnBy(['src/http/**', 'src/jobs/**'])
158
- ```
159
-
160
- Aliases are available for Pest-style wording: `toUse`, `toOnlyUse`,
161
- `toBeUsedIn`, `toOnlyBeUsedIn`, `toUseNothing`, and `toBeUsed`.
45
+ import { assertArchitecture } from "@lucasacoutinho/archer"
162
46
 
163
- Modifiers apply to the next dependency rule and then reset:
164
-
165
- ```ts
166
- archer
167
- .expect('src/application/**')
168
- .typeDependencies()
169
- .toOnlyDependOn('src/contracts/**')
170
- .runtimeDependencies()
171
- .not.toDependOn('src/testing/**')
172
- .dynamicDependencies()
173
- .not.toDependOn('src/admin/**')
174
- .localDependencies()
175
- .toOnlyDependOn(['src/application/**', 'src/domain/**'])
176
- .toTransitivelyDependOn('src/domain/events.ts')
47
+ test("architecture", () => assertArchitecture())
177
48
  ```
178
49
 
179
- Use `includingDependencyKinds('static', 'require')` for an exact combination,
180
- or `.transitively().toDependOn(...)` for explicit reachability. Transitive
181
- violations include the shortest discovered import chain.
182
-
183
- ### Selectors and resolution
184
-
185
- | Selector | Meaning |
186
- | --------------- | ------------------------------------------------ |
187
- | `src/domain/**` | Files resolved inside the configured root |
188
- | `npm:react` | An installed package and its subpaths |
189
- | `node:fs` | A Node built-in |
190
- | `bun:test` | A Bun built-in |
191
- | `unresolved:*` | Imports TypeScript could not resolve |
192
- | `dynamic:*` | Computed dynamic imports without a static target |
193
-
194
- Archer resolves path aliases, package exports, project references, workspace
195
- packages, symlinks, `.js` imports of TypeScript sources, re-exports,
196
- `import type`, import types, CommonJS `require`, and lazy imports.
197
-
198
- Literal lazy imports such as `import('./admin.js')` are normal resolved edges.
199
- A computed import such as `import(moduleName)` becomes `dynamic:unknown`.
200
- Negative boundary rules fail closed on that edge because the target cannot be
201
- proven safe; an allow-list can explicitly include `dynamic:*`.
202
-
203
- ## Named layers and public APIs
50
+ ## Selectors
204
51
 
205
- ```ts
206
- const layers = archer.layers({
207
- domain: 'src/domain/**',
208
- application: 'src/application/**',
209
- infrastructure: 'src/infrastructure/**',
210
- })
211
-
212
- layers.where('domain').mayNotDependOn('infrastructure')
213
- layers.where('application').mayOnlyDependOn('domain')
214
- layers.where('domain').mayOnlyBeUsedBy('application')
215
- layers.where('domain').exposeThrough('src/domain/index.ts')
216
- layers.toBeFreeOfCycles()
217
- ```
52
+ A selector is a string, an array of selectors (any of them), `{ not: … }`, or
53
+ `{ all: [...] }`.
218
54
 
219
- Layer rules only constrain local project dependencies. External packages do
220
- not need to be repeated in every layer allow-list. `exposeThrough` rejects
221
- deep imports into a layer from outside it.
55
+ | Text | Selects |
56
+ | --- | --- |
57
+ | `src/domain` | Every file at or under `src/domain` |
58
+ | `src/**/*.ts`, `src/*/index.ts`, `src/a?.ts` | Path globs: `**` spans directories, `*` and `?` stay within one name |
59
+ | `{src/a,src/b}/**` | Either alternative |
60
+ | `npm:react`, `npm:@scope/pkg`, `npm:*` | Installed packages, with any subpath |
61
+ | `node:fs`, `node:*`, `bun:*` | Built-in modules |
62
+ | `layer:domain` | The files of a layer |
63
+ | `unresolved:*` | Imports that TypeScript could not resolve |
64
+ | `computed:*` | `import(x)` and `require(x)` without a literal specifier |
222
65
 
223
- For directory-level cycles, collapse file nodes to a fixed depth:
224
-
225
- ```ts
226
- archer.expect('src/**').toBeFreeOfFolderCycles(2)
227
- ```
66
+ Paths are relative to the project root and always use `/`.
228
67
 
229
- ## TypeScript-aware rules
68
+ ## Rules
230
69
 
231
- Archer uses the configured TypeScript programs for declarations, resolved
232
- heritage, exports, decorators, method visibility, and global-call detection.
70
+ Every rule takes a stable ID first. Findings and baselines key on it.
233
71
 
234
- ```ts
235
- archer
236
- .expect('src/controllers/**')
237
- .toBeClasses()
238
- .toExtend('BaseController')
239
- .toImplement('Runnable')
240
- .toHaveDecorator('Controller')
241
- .toHaveReadonlyProperties()
242
- .toHaveMethods('execute')
243
- .toHavePublicMethods('execute')
244
- .toHaveProtectedMethods('reset')
245
- .toOnlyHavePublicMethods()
246
- .toBeExported()
247
-
248
- archer.expect('src/contracts/**').toBeInterfaces()
249
- archer.expect('src/base-controller.ts').toBeAbstract()
250
- archer.expect('src/status.ts').toBeEnums()
251
- archer.expect('src/factories/**').toBeFunctions()
252
- archer.expect('src/models/**').toBeTypes()
253
- archer.expect('src/index.ts').toOnlyExport('Client', 'ClientOptions')
254
- archer.expect('src/**').toUseStrictEquality()
255
- archer.expect('src/**').not.toUseGlobals('eval', 'Function')
256
- archer.expect('src/**').not.toUseImportedSymbols('react', 'useEffect')
257
- archer.expect('src/**').toHaveResolvableDependencies()
258
- archer.expect('src/index.ts').toBePublicApi()
72
+ ```text
73
+ r.forbid(id, subject, object, options?)
259
74
  ```
260
75
 
261
- Naming and size rules remain available through `toHavePrefix`,
262
- `toHaveSuffix`, and `toHaveLineCountLessThan`.
263
-
264
- These are static checks. Runtime dependency injection, reflection-generated
265
- exports, and targets hidden behind computed imports cannot be inferred.
76
+ | Rule | Meaning |
77
+ | --- | --- |
78
+ | `r.forbid` | The subject must not depend on the object |
79
+ | `r.require` | The subject must depend on something the object selects |
80
+ | `r.onlyDependOn` | The subject may depend only on the object |
81
+ | `r.forbidDependents` | Files the object selects must not depend on the subject |
82
+ | `r.requireDependents` | Some file the object selects must depend on the subject |
83
+ | `r.onlyDependedOnBy` | Only files the object selects may depend on the subject |
84
+ | `r.acyclic(id, subject, { folderDepth? })` | No import cycles, among files or among folders |
85
+ | `r.resolvable(id, subject, { allowComputed? })` | Every import resolves |
86
+ | `r.declares(id, subject, predicate)` / `r.declaresNone` | Every (or no) declaration satisfies a `p.*` predicate |
87
+ | `r.exportsOnly(id, subject, names)` | The files export only these names |
88
+ | `r.forbidUse(id, subject, use)` / `r.requireUse` | Calls and `==` checks, from `use.*` |
89
+ | `r.prefix` / `r.suffix(id, subject, text)` | File names start or end with `text` |
90
+ | `r.maxLines(id, subject, max)` | Files have at most `max` lines |
91
+ | `r.custom(id, subject, ({ graph, subject }) => messages)` | Your own check; each message is a finding |
266
92
 
267
- ### Imported API policies
93
+ Options for every rule: `severity` (`"error"`, `"warning"`, or `"info"`) and
94
+ `reason`. Dependency rules also take:
268
95
 
269
- `toUseImportedSymbols()` matches calls by their original module and export. It
270
- recognizes named-import aliases, namespace calls, default-import property
271
- calls, element access with a literal name, and CommonJS destructuring.
96
+ - `transitive: true` follows imports through other files. A finding then shows
97
+ the shortest import chain.
98
+ - `kinds` limits the import kinds: `"static"`, `"type"`, `"require"`, or
99
+ `"dynamic"`.
100
+ - `localOnly: true` ignores packages and built-ins.
272
101
 
273
- That keeps framework names out of Archer's rule engine. A React policy can be
274
- written from generic rules:
102
+ Predicates for `r.declares`:
275
103
 
276
104
  ```ts
277
- archer
278
- .expect('src/frontend/components/**/*.{ts,tsx}')
279
- .not.toBeClasses()
280
- .and.not.toUseImportedSymbols('react', 'useEffect', 'useContext')
105
+ import { p, use } from "@lucasacoutinho/archer"
281
106
 
282
- archer.expect('src/frontend/state/**').toDependOn('npm:zustand')
107
+ p.kind("class")
108
+ p.extends("Base")
109
+ p.implements("Runnable")
110
+ p.decorator("Controller")
111
+ p.hasMethods(["run"], "public")
112
+ p.onlyPublicMethods
113
+ p.abstract
114
+ p.exported
115
+ p.readonlyProperties
116
+ p.all(p.kind("class"), p.exported)
283
117
  ```
284
118
 
285
- The same call rule can reject Vue `provide` and `inject`, or Svelte
286
- `setContext` and `getContext`, in JavaScript and TypeScript modules:
119
+ Uses for `r.forbidUse`:
287
120
 
288
121
  ```ts
289
- archer.expect('src/vue/**').not.toUseImportedSymbols('vue', 'provide', 'inject')
290
- archer.expect('src/svelte/**').not.toUseImportedSymbols('svelte', 'setContext', 'getContext')
122
+ use.looseEquality
123
+ use.globals("eval")
124
+ use.imported("react", "useEffect")
291
125
  ```
292
126
 
293
- Vue and Svelte single-file components need their compiler parsers because a
294
- `.vue` or `.svelte` file is not a TypeScript source file. Archer does not parse
295
- those formats as TypeScript or claim that their template dependencies were
296
- checked. They belong in source-analyzer integrations, while the core rules
297
- remain framework-neutral.
127
+ Archer applies these semantics:
298
128
 
299
- ## Rule metadata, custom rules, and presets
129
+ - **Unknown imports fail closed.** An import whose target Archer cannot know
130
+ (`computed:*`) breaks every `forbid` rule. Allow it explicitly when you mean
131
+ it.
132
+ - **Layer rules check direct imports between declared layers.** Files outside
133
+ every layer are unconstrained.
134
+ - **Mistyped paths are listed, not failed.** A rule that selects no files is
135
+ listed as information under `emptySubjects`, and a rule whose target glob
136
+ matches nothing is listed under `emptyObjects`.
300
137
 
301
- Rules default to error severity. Stable IDs make baselines and CI annotations
302
- less sensitive to wording changes.
138
+ ## Configuration
303
139
 
304
- ```ts
305
- archer
306
- .expect('src/legacy/**')
307
- .not.toDependOn('src/new-core/**')
308
- .as('migration/no-new-core')
309
- .because('Legacy code is being removed, not expanded.')
310
- .withSeverity('warning')
311
- ```
140
+ | Field | Default | Meaning |
141
+ | --- | --- | --- |
142
+ | `layers`, `stack`, `allow` | | Layers and the dependencies allowed between them |
143
+ | `publicApis` | | `{ domain: "src/domain/index.ts" }`: other layers may import a layer only through its entry |
144
+ | `rules` | | Rules from `r.*` and `presets` |
145
+ | `root` | `.` | The project root, relative to the config file |
146
+ | `projects` | `["tsconfig.json"]` | The tsconfigs whose files Archer analyzes |
147
+ | `baseline` | `.archer-baseline` | The baseline file, or `false` |
148
+ | `failOnWarnings` | `false` | Whether warnings fail the check |
312
149
 
313
- Custom rules may be synchronous or asynchronous:
150
+ ## CLI
314
151
 
315
- ```ts
316
- archer.expect('src/features/**').toSatisfy('features/one-entrypoint', ({ modules }) =>
317
- modules.flatMap((module) =>
318
- module.exports.length > 1
319
- ? [
320
- {
321
- file: module.path,
322
- message: 'Feature exports more than one symbol.',
323
- rule: 'features expose one entrypoint',
324
- severity: 'error',
325
- },
326
- ]
327
- : [],
328
- ),
329
- )
330
- ```
152
+ | Command | Does |
153
+ | --- | --- |
154
+ | `archer check` | Checks and prints a report |
155
+ | `archer check -w` | Checks again after every change, in a fresh process |
156
+ | `archer baseline record` | Accepts every current violation |
157
+ | `archer baseline prune` | Drops baseline entries that no violation has |
158
+ | `archer graph -o graph.json` | Saves the analysis. `archer check --graph graph.json` checks it without TypeScript |
159
+ | `archer init` | Writes a starter config |
331
160
 
332
- Built-in presets are `strict`, `node`, and `security`:
161
+ `check` options:
333
162
 
334
- ```ts
335
- archer.preset('strict')
336
- archer.preset('node')
163
+ | Option | Meaning |
164
+ | --- | --- |
165
+ | `-c, --config <file>` | The config file. Default: `archer.config.{ts,mts,js,mjs}` |
166
+ | `-r, --reporter <name>` | `pretty`, `agent` (one line per finding, for tools and agents), `json`, `sarif`, `github` (annotations), or `dot` (the import graph) |
167
+ | `-o, --output <file>` | Write the report to a file |
168
+ | `--ignore-baseline` | Report every violation |
337
169
 
338
- registerPreset('company', (suite) => {
339
- suite.expect('src/**').not.toDependOn('npm:deprecated-sdk')
340
- })
341
- ```
170
+ ## Baselines
342
171
 
343
- ## Monorepos and project references
172
+ To adopt Archer in a codebase with existing violations, run
173
+ `archer baseline record` and commit `.archer-baseline`. Later checks fail
174
+ only on new violations. When you fix old violations, run
175
+ `archer baseline prune`.
344
176
 
345
- Archer discovers npm/Yarn and pnpm workspaces plus referenced tsconfigs. You
346
- can also declare projects and override their source selection:
177
+ ## Programmatic use
347
178
 
348
179
  ```ts
349
- export default defineConfig({
350
- projects: [
351
- 'packages/core/tsconfig.json',
352
- {
353
- tsconfig: 'apps/*/tsconfig.json',
354
- include: ['src/**/*.ts', 'scripts/**/*.ts'],
355
- },
356
- ],
357
- rules(archer) {
358
- archer.expect('packages/**').toBeFreeOfCycles()
359
- },
360
- })
361
- ```
180
+ import { check, graphOf, run } from "@lucasacoutinho/archer"
181
+ import config from "./archer.config.ts"
362
182
 
363
- Set `workspaces: false`, `followSymlinks: false`, `cache: false`, or
364
- `tsconfig: false` to override the corresponding defaults.
183
+ const { report } = await run({ config: "archer.config.ts" })
365
184
 
366
- ## Reports and baselines
367
-
368
- The CLI supports `pretty`, `agent`, `llm`, `json`, `sarif`, `github`, and `dot` reporters.
369
- `agent` and its `llm` alias emit one stable, single-line record per finding without ANSI output:
370
-
371
- ```sh
372
- npx archer check --reporter agent
373
- npx archer check --reporter sarif --output reports/archer.sarif
374
- npx archer check --reporter github
375
- npx archer check --reporter dot --output reports/dependencies.dot
376
- ```
377
-
378
- Adopt Archer in an existing codebase without accepting new violations:
379
-
380
- ```sh
381
- npx archer check --update-baseline
382
- npx archer check
185
+ // Test a policy against a graph in memory, without TypeScript:
186
+ const result = check(config, graphOf({ "src/domain/a.ts": ["src/app/b.ts"], "src/app/b.ts": [] }))
383
187
  ```
384
188
 
385
- The default baseline is `.archer-baseline.json`. Use `--baseline <file>` or
386
- the config `baseline` field to choose another path. `--fail-on-warnings` makes
387
- warning-level rules affect the CLI exit code.
388
-
389
- The core API exposes the same data for integrations:
390
-
391
- ```ts
392
- import { ArchitectureError, createArcher } from '@lucasacoutinho/archer'
393
-
394
- const archer = createArcher({ root: process.cwd() })
395
- archer.expect('src/**').toBeFreeOfCycles()
396
-
397
- const result = await archer.check()
398
- if (!result.ok) throw new ArchitectureError(result)
399
- ```
189
+ `@lucasacoutinho/archer/effect` exposes the same operations as Effect values.
190
+ It also exposes the `Analyzer` service, with `typescript`, `memory`, and
191
+ `replay` adapters.
400
192
 
401
193
  ## Agent skill
402
194
 
403
- Archer ships a portable [Agent Skill](https://openagentskills.dev/) in
404
- `skills/archer`. Its source is versioned with the engine and included in the npm
405
- package, so it is not tied to one developer's local agent configuration.
406
-
407
- Install it into any supported coding agent with the Skills CLI:
408
-
409
- ```sh
410
- npx skills add lucasacoutinho/archer --skill archer
411
- ```
412
-
413
- Agents can then use `$archer` to set up rules, diagnose findings, and fix source
414
- violations without weakening architecture policy.
195
+ [`skills/archer`](skills/archer/SKILL.md) teaches a coding agent to set up
196
+ Archer, read `archer check -r agent`, and fix violations in source. Install it
197
+ with `npx skills add lucasacoutinho/archer --skill archer`.
415
198
 
416
199
  ## Development
417
200
 
418
- Archer dogfoods its rules in both `test/architecture.test.ts` and
419
- `archer.config.ts`. The repository checks Node 20, 22, and 24 on Linux and
420
- Windows, plus Jest, Bun, `node:test`, the CLI, package exports, and type
421
- definitions.
422
-
423
201
  ```sh
424
- npm install
425
- npm run fmt
426
- npm run lint
427
- npm run check
428
- npm run check:package
429
- npm run release:check
202
+ npm install # also patches tsc with @effect/tsgo
203
+ npm run check # Lean proofs, type check, tests, build, Archer on itself
430
204
  ```
431
205
 
432
- To cross-test the adjacent tiacore checkout, build tiacore once and run:
206
+ `npm run check` needs [elan](https://github.com/leanprover/elan): Lean is
207
+ pinned in `lean/lean-toolchain`. See [architecture](docs/architecture.md) for
208
+ how Archer is built, and [verification](docs/verification.md) for what is
209
+ proved and how the proofs reach the code.
433
210
 
434
- ```sh
435
- npm run test:tia
436
- npm run test:tia:verify
437
- ```
438
-
439
- The scripts prefer an installed `@lucasacoutinho/tiacore` package and otherwise
440
- use `../tiacore`. Set `TIACORE_VITEST_ENTRY` or `TIACORE_CLI` for a checkout in
441
- another location.
442
-
443
- ## Releasing
444
-
445
- Releases publish to the public npm registry from the `Publish package` workflow
446
- (`.github/workflows/publish.yml`) with npm trusted publishing, so no npm token is
447
- stored in the repository and npm attaches provenance automatically. npm can only
448
- attach a trusted publisher to a package that already exists, so the first
449
- version is published manually by a maintainer with two-factor authentication.
450
- After that, link the workflow once:
451
-
452
- ```sh
453
- npm trust github @lucasacoutinho/archer --file publish.yml --repo lucasacoutinho/archer --allow-publish
454
- ```
211
+ Releases publish from the `Publish package` workflow when you publish a
212
+ GitHub release. The workflow uses npm trusted publishing with provenance.
213
+ Add the release to [CHANGELOG.md](CHANGELOG.md) first.
455
214
 
456
215
  ## License
457
216
 
458
- Archer is licensed under the [Apache License, Version 2.0](LICENSE). See
459
- [NOTICE](NOTICE) for attribution.
217
+ [Apache 2.0](LICENSE). See [NOTICE](NOTICE).
@@ -0,0 +1,17 @@
1
+ /** The only part of Archer that talks to TypeScript. Contract: docs/architecture.md#analyzer. */
2
+ import { Context, FileSystem, Layer, type Path } from "effect";
3
+ import type { Graph } from "../core/model.ts";
4
+ import { AnalyzerUnavailable, type AnalysisError, GraphFileInvalid, ProjectNotFound, ProjectParseFailed } from "./errors.ts";
5
+ import type { AnalyzerShape } from "./workspace.ts";
6
+ export { AbsolutePath, RootRelativePath, Workspace } from "./workspace.ts";
7
+ export { AnalyzerUnavailable, GraphFileInvalid, ProjectNotFound, ProjectParseFailed };
8
+ export type { AnalysisError };
9
+ declare const Analyzer_base: Context.ServiceClass<Analyzer, "archer/Analyzer", AnalyzerShape>;
10
+ export declare class Analyzer extends Analyzer_base {
11
+ /** TypeScript's native API. The server starts on the first `analyze` and stops with the layer's scope. */
12
+ static readonly typescript: Layer.Layer<Analyzer, never, FileSystem.FileSystem | Path.Path>;
13
+ static readonly memory: (graph: Graph) => Layer.Layer<Analyzer>;
14
+ /** A graph saved by `archer graph`, read again on every `analyze`; the workspace is not consulted. */
15
+ static readonly replay: (path: string) => Layer.Layer<Analyzer, never, FileSystem.FileSystem>;
16
+ }
17
+ //# sourceMappingURL=Analyzer.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"Analyzer.d.ts","sourceRoot":"","sources":["../../src/analyzer/Analyzer.ts"],"names":[],"mappings":"AAAA,iGAAiG;AACjG,OAAO,EAAE,OAAO,EAAU,UAAU,EAAE,KAAK,EAAE,KAAK,IAAI,EAAE,MAAM,QAAQ,CAAA;AACtE,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,kBAAkB,CAAA;AAC7C,OAAO,EAAE,mBAAmB,EAAE,KAAK,aAAa,EAAE,gBAAgB,EAAE,eAAe,EAAE,kBAAkB,EAAE,MAAM,aAAa,CAAA;AAG5H,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,gBAAgB,CAAA;AAEnD,OAAO,EAAE,YAAY,EAAE,gBAAgB,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAA;AAE1E,OAAO,EAAE,mBAAmB,EAAE,gBAAgB,EAAE,eAAe,EAAE,kBAAkB,EAAE,CAAA;AACrF,YAAY,EAAE,aAAa,EAAE,CAAA;;AAE7B,qBAAa,QAAS,SAAQ,aAA6D;IACzF,0GAA0G;IAC1G,MAAM,CAAC,QAAQ,CAAC,UAAU,EAAE,KAAK,CAAC,KAAK,CAAC,QAAQ,EAAE,KAAK,EAAE,UAAU,CAAC,UAAU,GAAG,IAAI,CAAC,IAAI,CAAC,CAAyC;IAEpI,MAAM,CAAC,QAAQ,CAAC,MAAM,UAAW,KAAK,KAAG,KAAK,CAAC,KAAK,CAAC,QAAQ,CAAC,CACkB;IAEhF,sGAAsG;IACtG,MAAM,CAAC,QAAQ,CAAC,MAAM,SAAU,MAAM,KAAG,KAAK,CAAC,KAAK,CAAC,QAAQ,EAAE,KAAK,EAAE,UAAU,CAAC,UAAU,CAAC,CAMzF;CACJ"}
@@ -0,0 +1,15 @@
1
+ /** The only part of Archer that talks to TypeScript. Contract: docs/architecture.md#analyzer. */
2
+ import { Context, Effect, FileSystem, Layer } from "effect";
3
+ import { AnalyzerUnavailable, GraphFileInvalid, ProjectNotFound, ProjectParseFailed } from "./errors.js";
4
+ import { readGraphFile } from "./graph-file.js";
5
+ import { makeTypescript } from "./typescript.js";
6
+ export { AbsolutePath, RootRelativePath, Workspace } from "./workspace.js";
7
+ export { AnalyzerUnavailable, GraphFileInvalid, ProjectNotFound, ProjectParseFailed };
8
+ export class Analyzer extends Context.Service()("archer/Analyzer") {
9
+ /** TypeScript's native API. The server starts on the first `analyze` and stops with the layer's scope. */
10
+ static typescript = Layer.effect(Analyzer, makeTypescript);
11
+ static memory = (graph) => Layer.succeed(Analyzer, Analyzer.of({ analyze: () => Effect.succeed(graph) }));
12
+ /** A graph saved by `archer graph`, read again on every `analyze`; the workspace is not consulted. */
13
+ static replay = (path) => Layer.effect(Analyzer, Effect.map(FileSystem.FileSystem, (fs) => Analyzer.of({ analyze: () => readGraphFile(path).pipe(Effect.provideService(FileSystem.FileSystem, fs)) })));
14
+ }
15
+ //# sourceMappingURL=Analyzer.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"Analyzer.js","sourceRoot":"","sources":["../../src/analyzer/Analyzer.ts"],"names":[],"mappings":"AAAA,iGAAiG;AACjG,OAAO,EAAE,OAAO,EAAE,MAAM,EAAE,UAAU,EAAE,KAAK,EAAa,MAAM,QAAQ,CAAA;AAEtE,OAAO,EAAE,mBAAmB,EAAsB,gBAAgB,EAAE,eAAe,EAAE,kBAAkB,EAAE,MAAM,aAAa,CAAA;AAC5H,OAAO,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAA;AAC/C,OAAO,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAA;AAGhD,OAAO,EAAE,YAAY,EAAE,gBAAgB,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAA;AAE1E,OAAO,EAAE,mBAAmB,EAAE,gBAAgB,EAAE,eAAe,EAAE,kBAAkB,EAAE,CAAA;AAGrF,MAAM,OAAO,QAAS,SAAQ,OAAO,CAAC,OAAO,EAA2B,CAAC,iBAAiB,CAAC;IACzF,0GAA0G;IAC1G,MAAM,CAAU,UAAU,GAAoE,KAAK,CAAC,MAAM,CAAC,QAAQ,EAAE,cAAc,CAAC,CAAA;IAEpI,MAAM,CAAU,MAAM,GAAG,CAAC,KAAY,EAAyB,EAAE,CAC/D,KAAK,CAAC,OAAO,CAAC,QAAQ,EAAE,QAAQ,CAAC,EAAE,CAAC,EAAE,OAAO,EAAE,GAAG,EAAE,CAAC,MAAM,CAAC,OAAO,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC,CAAA;IAEhF,sGAAsG;IACtG,MAAM,CAAU,MAAM,GAAG,CAAC,IAAY,EAAuD,EAAE,CAC7F,KAAK,CAAC,MAAM,CACV,QAAQ,EACR,MAAM,CAAC,GAAG,CAAC,UAAU,CAAC,UAAU,EAAE,CAAC,EAAE,EAAE,EAAE,CACvC,QAAQ,CAAC,EAAE,CAAC,EAAE,OAAO,EAAE,GAAG,EAAE,CAAC,aAAa,CAAC,IAAI,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC,cAAc,CAAC,UAAU,CAAC,UAAU,EAAE,EAAE,CAAC,CAAC,EAAE,CAAC,CAC3G,CACF,CAAA;CACJ"}