@lucasacoutinho/archer 0.2.1 → 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 +144 -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,460 +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
8
  npm install --save-dev @lucasacoutinho/archer
14
9
  ```
15
10
 
16
- Archer supports Node.js 20 and newer. It parses sources with its own bundled
17
- TypeScript, so the project's TypeScript version does not matter. The core
18
- package has no test-runner dependency.
19
-
20
- ## Start with the CLI
21
-
22
- Create `archer.config.ts`:
23
-
24
- ```ts
25
- import { defineConfig } from '@lucasacoutinho/archer'
26
-
27
- export default defineConfig({
28
- include: ['src/**/*.ts', 'packages/*/src/**/*.ts'],
29
- exclude: ['**/*.generated.ts'],
11
+ Archer needs Node 22.18 or newer and is ESM only.
30
12
 
31
- rules(archer) {
32
- archer
33
- .expect('src/domain/**')
34
- .not.toDependOn('src/infrastructure/**')
35
- .because('The domain must remain portable.')
36
- .as('boundaries/domain-isolation')
37
-
38
- archer.expect('src/**').toBeFreeOfCycles()
39
- },
40
- })
41
- ```
42
-
43
- Then run:
13
+ ## Quick start
44
14
 
45
15
  ```sh
46
- npx archer check
47
- npx archer check --watch
48
- ```
49
-
50
- `archer init` creates a starter config. Config paths, output paths, and the
51
- project root are resolved relative to the config file.
52
-
53
- ## Test-runner adapters
54
-
55
- Each adapter registers a test immediately and returns the same rule builder.
56
-
57
- ```ts
58
- // test/architecture.test.ts
59
- import { arch } from '@lucasacoutinho/archer/vitest'
60
-
61
- arch('the domain stays independent')
62
- .expect('src/domain/**')
63
- .not.toDependOn('src/infrastructure/**')
64
- .and.toOnlyDependOn(['src/domain/**', 'src/shared/**'])
65
- ```
66
-
67
- The other entry points are:
68
-
69
- ```ts
70
- import { arch } from '@lucasacoutinho/archer/jest'
71
- import { arch } from '@lucasacoutinho/archer/bun'
72
- 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
73
18
  ```
74
19
 
75
- Jest and Bun accept `timeout` as an Archer adapter option. The Node adapter
76
- accepts built-in `node:test` options under `test`.
77
-
78
- ## Vitest config plugin
79
-
80
- Vitest can run Archer once, before it starts collecting tests. The plugin loads
81
- `archer.config.*` from the Vitest project root by default:
82
-
83
20
  ```ts
84
- // vitest.config.ts
85
- import { archer } from '@lucasacoutinho/archer/vitest'
86
- import { defineConfig } from 'vitest/config'
21
+ // archer.config.ts
22
+ import { defineConfig, presets, r } from "@lucasacoutinho/archer"
87
23
 
88
24
  export default defineConfig({
89
- plugins: [archer()],
90
- })
91
- ```
92
-
93
- Pass `config`, `reporter`, `baseline`, `output`, or `failOnWarnings` to
94
- `archer()` when the defaults are not enough. This is a configuration plugin,
95
- so it performs one architecture check for the Vitest project instead of
96
- registering a test in every worker.
97
-
98
- ## Matcher setup modules
99
-
100
- Vitest, Jest, and Bun can also install Archer as an `expect` plugin. This keeps
101
- the runner integration in its normal setup hook and lets architecture tests use
102
- the same assertion in every runner.
103
-
104
- For Vitest, add the plugin to `setupFiles`:
105
-
106
- ```ts
107
- // vitest.config.ts
108
- import { defineConfig } from 'vitest/config'
109
-
110
- export default defineConfig({
111
- test: {
112
- setupFiles: ['@lucasacoutinho/archer/vitest/plugin'],
25
+ layers: {
26
+ app: "src/app",
27
+ domain: "src/domain",
28
+ infrastructure: "src/infrastructure",
113
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
+ ],
114
38
  })
115
39
  ```
116
40
 
117
- For Jest, add it to `setupFilesAfterEnv`:
118
-
119
- ```ts
120
- // jest.config.ts
121
- export default {
122
- setupFilesAfterEnv: ['@lucasacoutinho/archer/jest/plugin'],
123
- }
124
- ```
125
-
126
- For Bun, preload it from `bunfig.toml`:
127
-
128
- ```toml
129
- [test]
130
- preload = ["@lucasacoutinho/archer/bun/plugin"]
131
- ```
132
-
133
- Then create a normal test and await the asynchronous matcher:
134
-
135
- ```ts
136
- import { createArcher } from '@lucasacoutinho/archer'
137
-
138
- test('the architecture is respected', async () => {
139
- const archer = createArcher()
140
- archer.expect('src/domain/**').not.toDependOn('src/infrastructure/**')
141
-
142
- await expect(archer).toRespectArchitecture()
143
- })
144
- ```
145
-
146
- The plugin installs the matcher; it does not silently inject a test into every
147
- worker. Use the `arch()` adapter when the fluent one-call form is preferable.
148
-
149
- ## 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:
150
43
 
151
44
  ```ts
152
- archer
153
- .expect('src/application/**')
154
- .toDependOn('src/domain/**')
155
- .not.toDependOn('src/web/**')
156
- .toOnlyDependOn(['src/application/**', 'src/domain/**', 'npm:zod'])
157
- .toBeDependedOnBy('src/http/**')
158
- .toOnlyBeDependedOnBy(['src/http/**', 'src/jobs/**'])
159
- ```
160
-
161
- Aliases are available for Pest-style wording: `toUse`, `toOnlyUse`,
162
- `toBeUsedIn`, `toOnlyBeUsedIn`, `toUseNothing`, and `toBeUsed`.
45
+ import { assertArchitecture } from "@lucasacoutinho/archer"
163
46
 
164
- Modifiers apply to the next dependency rule and then reset:
165
-
166
- ```ts
167
- archer
168
- .expect('src/application/**')
169
- .typeDependencies()
170
- .toOnlyDependOn('src/contracts/**')
171
- .runtimeDependencies()
172
- .not.toDependOn('src/testing/**')
173
- .dynamicDependencies()
174
- .not.toDependOn('src/admin/**')
175
- .localDependencies()
176
- .toOnlyDependOn(['src/application/**', 'src/domain/**'])
177
- .toTransitivelyDependOn('src/domain/events.ts')
47
+ test("architecture", () => assertArchitecture())
178
48
  ```
179
49
 
180
- Use `includingDependencyKinds('static', 'require')` for an exact combination,
181
- or `.transitively().toDependOn(...)` for explicit reachability. Transitive
182
- violations include the shortest discovered import chain.
183
-
184
- ### Selectors and resolution
185
-
186
- | Selector | Meaning |
187
- | --------------- | ------------------------------------------------ |
188
- | `src/domain/**` | Files resolved inside the configured root |
189
- | `npm:react` | An installed package and its subpaths |
190
- | `node:fs` | A Node built-in |
191
- | `bun:test` | A Bun built-in |
192
- | `unresolved:*` | Imports TypeScript could not resolve |
193
- | `dynamic:*` | Computed dynamic imports without a static target |
194
-
195
- Archer resolves path aliases, package exports, project references, workspace
196
- packages, symlinks, `.js` imports of TypeScript sources, re-exports,
197
- `import type`, import types, CommonJS `require`, and lazy imports.
198
-
199
- Literal lazy imports such as `import('./admin.js')` are normal resolved edges.
200
- A computed import such as `import(moduleName)` becomes `dynamic:unknown`.
201
- Negative boundary rules fail closed on that edge because the target cannot be
202
- proven safe; an allow-list can explicitly include `dynamic:*`.
203
-
204
- ## Named layers and public APIs
50
+ ## Selectors
205
51
 
206
- ```ts
207
- const layers = archer.layers({
208
- domain: 'src/domain/**',
209
- application: 'src/application/**',
210
- infrastructure: 'src/infrastructure/**',
211
- })
212
-
213
- layers.where('domain').mayNotDependOn('infrastructure')
214
- layers.where('application').mayOnlyDependOn('domain')
215
- layers.where('domain').mayOnlyBeUsedBy('application')
216
- layers.where('domain').exposeThrough('src/domain/index.ts')
217
- layers.toBeFreeOfCycles()
218
- ```
52
+ A selector is a string, an array of selectors (any of them), `{ not: … }`, or
53
+ `{ all: [...] }`.
219
54
 
220
- Layer rules only constrain local project dependencies. External packages do
221
- not need to be repeated in every layer allow-list. `exposeThrough` rejects
222
- 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 |
223
65
 
224
- For directory-level cycles, collapse file nodes to a fixed depth:
225
-
226
- ```ts
227
- archer.expect('src/**').toBeFreeOfFolderCycles(2)
228
- ```
66
+ Paths are relative to the project root and always use `/`.
229
67
 
230
- ## TypeScript-aware rules
68
+ ## Rules
231
69
 
232
- Archer uses the configured TypeScript programs for declarations, resolved
233
- heritage, exports, decorators, method visibility, and global-call detection.
70
+ Every rule takes a stable ID first. Findings and baselines key on it.
234
71
 
235
- ```ts
236
- archer
237
- .expect('src/controllers/**')
238
- .toBeClasses()
239
- .toExtend('BaseController')
240
- .toImplement('Runnable')
241
- .toHaveDecorator('Controller')
242
- .toHaveReadonlyProperties()
243
- .toHaveMethods('execute')
244
- .toHavePublicMethods('execute')
245
- .toHaveProtectedMethods('reset')
246
- .toOnlyHavePublicMethods()
247
- .toBeExported()
248
-
249
- archer.expect('src/contracts/**').toBeInterfaces()
250
- archer.expect('src/base-controller.ts').toBeAbstract()
251
- archer.expect('src/status.ts').toBeEnums()
252
- archer.expect('src/factories/**').toBeFunctions()
253
- archer.expect('src/models/**').toBeTypes()
254
- archer.expect('src/index.ts').toOnlyExport('Client', 'ClientOptions')
255
- archer.expect('src/**').toUseStrictEquality()
256
- archer.expect('src/**').not.toUseGlobals('eval', 'Function')
257
- archer.expect('src/**').not.toUseImportedSymbols('react', 'useEffect')
258
- archer.expect('src/**').toHaveResolvableDependencies()
259
- archer.expect('src/index.ts').toBePublicApi()
72
+ ```text
73
+ r.forbid(id, subject, object, options?)
260
74
  ```
261
75
 
262
- Naming and size rules remain available through `toHavePrefix`,
263
- `toHaveSuffix`, and `toHaveLineCountLessThan`.
264
-
265
- These are static checks. Runtime dependency injection, reflection-generated
266
- 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 |
267
92
 
268
- ### Imported API policies
93
+ Options for every rule: `severity` (`"error"`, `"warning"`, or `"info"`) and
94
+ `reason`. Dependency rules also take:
269
95
 
270
- `toUseImportedSymbols()` matches calls by their original module and export. It
271
- recognizes named-import aliases, namespace calls, default-import property
272
- 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.
273
101
 
274
- That keeps framework names out of Archer's rule engine. A React policy can be
275
- written from generic rules:
102
+ Predicates for `r.declares`:
276
103
 
277
104
  ```ts
278
- archer
279
- .expect('src/frontend/components/**/*.{ts,tsx}')
280
- .not.toBeClasses()
281
- .and.not.toUseImportedSymbols('react', 'useEffect', 'useContext')
105
+ import { p, use } from "@lucasacoutinho/archer"
282
106
 
283
- 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)
284
117
  ```
285
118
 
286
- The same call rule can reject Vue `provide` and `inject`, or Svelte
287
- `setContext` and `getContext`, in JavaScript and TypeScript modules:
119
+ Uses for `r.forbidUse`:
288
120
 
289
121
  ```ts
290
- archer.expect('src/vue/**').not.toUseImportedSymbols('vue', 'provide', 'inject')
291
- archer.expect('src/svelte/**').not.toUseImportedSymbols('svelte', 'setContext', 'getContext')
122
+ use.looseEquality
123
+ use.globals("eval")
124
+ use.imported("react", "useEffect")
292
125
  ```
293
126
 
294
- Vue and Svelte single-file components need their compiler parsers because a
295
- `.vue` or `.svelte` file is not a TypeScript source file. Archer does not parse
296
- those formats as TypeScript or claim that their template dependencies were
297
- checked. They belong in source-analyzer integrations, while the core rules
298
- remain framework-neutral.
127
+ Archer applies these semantics:
299
128
 
300
- ## 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`.
301
137
 
302
- Rules default to error severity. Stable IDs make baselines and CI annotations
303
- less sensitive to wording changes.
138
+ ## Configuration
304
139
 
305
- ```ts
306
- archer
307
- .expect('src/legacy/**')
308
- .not.toDependOn('src/new-core/**')
309
- .as('migration/no-new-core')
310
- .because('Legacy code is being removed, not expanded.')
311
- .withSeverity('warning')
312
- ```
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 |
313
149
 
314
- Custom rules may be synchronous or asynchronous:
150
+ ## CLI
315
151
 
316
- ```ts
317
- archer.expect('src/features/**').toSatisfy('features/one-entrypoint', ({ modules }) =>
318
- modules.flatMap((module) =>
319
- module.exports.length > 1
320
- ? [
321
- {
322
- file: module.path,
323
- message: 'Feature exports more than one symbol.',
324
- rule: 'features expose one entrypoint',
325
- severity: 'error',
326
- },
327
- ]
328
- : [],
329
- ),
330
- )
331
- ```
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 |
332
160
 
333
- Built-in presets are `strict`, `node`, and `security`:
161
+ `check` options:
334
162
 
335
- ```ts
336
- archer.preset('strict')
337
- 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 |
338
169
 
339
- registerPreset('company', (suite) => {
340
- suite.expect('src/**').not.toDependOn('npm:deprecated-sdk')
341
- })
342
- ```
170
+ ## Baselines
343
171
 
344
- ## 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`.
345
176
 
346
- Archer discovers npm/Yarn and pnpm workspaces plus referenced tsconfigs. You
347
- can also declare projects and override their source selection:
177
+ ## Programmatic use
348
178
 
349
179
  ```ts
350
- export default defineConfig({
351
- projects: [
352
- 'packages/core/tsconfig.json',
353
- {
354
- tsconfig: 'apps/*/tsconfig.json',
355
- include: ['src/**/*.ts', 'scripts/**/*.ts'],
356
- },
357
- ],
358
- rules(archer) {
359
- archer.expect('packages/**').toBeFreeOfCycles()
360
- },
361
- })
362
- ```
180
+ import { check, graphOf, run } from "@lucasacoutinho/archer"
181
+ import config from "./archer.config.ts"
363
182
 
364
- Set `workspaces: false`, `followSymlinks: false`, `cache: false`, or
365
- `tsconfig: false` to override the corresponding defaults.
183
+ const { report } = await run({ config: "archer.config.ts" })
366
184
 
367
- ## Reports and baselines
368
-
369
- The CLI supports `pretty`, `agent`, `llm`, `json`, `sarif`, `github`, and `dot` reporters.
370
- `agent` and its `llm` alias emit one stable, single-line record per finding without ANSI output:
371
-
372
- ```sh
373
- npx archer check --reporter agent
374
- npx archer check --reporter sarif --output reports/archer.sarif
375
- npx archer check --reporter github
376
- npx archer check --reporter dot --output reports/dependencies.dot
377
- ```
378
-
379
- Adopt Archer in an existing codebase without accepting new violations:
380
-
381
- ```sh
382
- npx archer check --update-baseline
383
- 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": [] }))
384
187
  ```
385
188
 
386
- The default baseline is `.archer-baseline.json`. Use `--baseline <file>` or
387
- the config `baseline` field to choose another path. `--fail-on-warnings` makes
388
- warning-level rules affect the CLI exit code.
389
-
390
- The core API exposes the same data for integrations:
391
-
392
- ```ts
393
- import { ArchitectureError, createArcher } from '@lucasacoutinho/archer'
394
-
395
- const archer = createArcher({ root: process.cwd() })
396
- archer.expect('src/**').toBeFreeOfCycles()
397
-
398
- const result = await archer.check()
399
- if (!result.ok) throw new ArchitectureError(result)
400
- ```
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.
401
192
 
402
193
  ## Agent skill
403
194
 
404
- Archer ships a portable [Agent Skill](https://openagentskills.dev/) in
405
- `skills/archer`. Its source is versioned with the engine and included in the npm
406
- package, so it is not tied to one developer's local agent configuration.
407
-
408
- Install it into any supported coding agent with the Skills CLI:
409
-
410
- ```sh
411
- npx skills add lucasacoutinho/archer --skill archer
412
- ```
413
-
414
- Agents can then use `$archer` to set up rules, diagnose findings, and fix source
415
- 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`.
416
198
 
417
199
  ## Development
418
200
 
419
- Archer dogfoods its rules in both `test/architecture.test.ts` and
420
- `archer.config.ts`. The repository checks Node 20, 22, and 24 on Linux and
421
- Windows, plus Jest, Bun, `node:test`, the CLI, package exports, and type
422
- definitions.
423
-
424
201
  ```sh
425
- npm install
426
- npm run fmt
427
- npm run lint
428
- npm run check
429
- npm run check:package
430
- 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
431
204
  ```
432
205
 
433
- 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.
434
210
 
435
- ```sh
436
- npm run test:tia
437
- npm run test:tia:verify
438
- ```
439
-
440
- The scripts prefer an installed `@lucasacoutinho/tiacore` package and otherwise
441
- use `../tiacore`. Set `TIACORE_VITEST_ENTRY` or `TIACORE_CLI` for a checkout in
442
- another location.
443
-
444
- ## Releasing
445
-
446
- Releases publish to the public npm registry from the `Publish package` workflow
447
- (`.github/workflows/publish.yml`) with npm trusted publishing, so no npm token is
448
- stored in the repository and npm attaches provenance automatically. npm can only
449
- attach a trusted publisher to a package that already exists, so the first
450
- version is published manually by a maintainer with two-factor authentication.
451
- After that, link the workflow once:
452
-
453
- ```sh
454
- npm trust github @lucasacoutinho/archer --file publish.yml --repo lucasacoutinho/archer --allow-publish
455
- ```
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.
456
214
 
457
215
  ## License
458
216
 
459
- Archer is licensed under the [Apache License, Version 2.0](LICENSE). See
460
- [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"}