archcheck 0.2.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 (251) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +536 -0
  3. package/dist/archcheck.d.ts +99 -0
  4. package/dist/archcheck.d.ts.map +1 -0
  5. package/dist/archcheck.js +339 -0
  6. package/dist/archcheck.js.map +1 -0
  7. package/dist/baseline/index.d.ts +29 -0
  8. package/dist/baseline/index.d.ts.map +1 -0
  9. package/dist/baseline/index.js +116 -0
  10. package/dist/baseline/index.js.map +1 -0
  11. package/dist/cli/args.d.ts +36 -0
  12. package/dist/cli/args.d.ts.map +1 -0
  13. package/dist/cli/args.js +233 -0
  14. package/dist/cli/args.js.map +1 -0
  15. package/dist/cli/index.d.ts +4 -0
  16. package/dist/cli/index.d.ts.map +1 -0
  17. package/dist/cli/index.js +219 -0
  18. package/dist/cli/index.js.map +1 -0
  19. package/dist/cli/version.d.ts +2 -0
  20. package/dist/cli/version.d.ts.map +1 -0
  21. package/dist/cli/version.js +29 -0
  22. package/dist/cli/version.js.map +1 -0
  23. package/dist/contracts/banned-syntax.d.ts +48 -0
  24. package/dist/contracts/banned-syntax.d.ts.map +1 -0
  25. package/dist/contracts/banned-syntax.js +201 -0
  26. package/dist/contracts/banned-syntax.js.map +1 -0
  27. package/dist/contracts/declared-field.d.ts +39 -0
  28. package/dist/contracts/declared-field.d.ts.map +1 -0
  29. package/dist/contracts/declared-field.js +142 -0
  30. package/dist/contracts/declared-field.js.map +1 -0
  31. package/dist/contracts/encapsulation.d.ts +28 -0
  32. package/dist/contracts/encapsulation.d.ts.map +1 -0
  33. package/dist/contracts/encapsulation.js +130 -0
  34. package/dist/contracts/encapsulation.js.map +1 -0
  35. package/dist/contracts/index.d.ts +10 -0
  36. package/dist/contracts/index.d.ts.map +1 -0
  37. package/dist/contracts/index.js +9 -0
  38. package/dist/contracts/index.js.map +1 -0
  39. package/dist/contracts/judgement.d.ts +21 -0
  40. package/dist/contracts/judgement.d.ts.map +1 -0
  41. package/dist/contracts/judgement.js +109 -0
  42. package/dist/contracts/judgement.js.map +1 -0
  43. package/dist/contracts/layers.d.ts +75 -0
  44. package/dist/contracts/layers.d.ts.map +1 -0
  45. package/dist/contracts/layers.js +244 -0
  46. package/dist/contracts/layers.js.map +1 -0
  47. package/dist/contracts/naming.d.ts +21 -0
  48. package/dist/contracts/naming.d.ts.map +1 -0
  49. package/dist/contracts/naming.js +120 -0
  50. package/dist/contracts/naming.js.map +1 -0
  51. package/dist/contracts/registry.d.ts +23 -0
  52. package/dist/contracts/registry.d.ts.map +1 -0
  53. package/dist/contracts/registry.js +48 -0
  54. package/dist/contracts/registry.js.map +1 -0
  55. package/dist/contracts/required-file.d.ts +29 -0
  56. package/dist/contracts/required-file.d.ts.map +1 -0
  57. package/dist/contracts/required-file.js +131 -0
  58. package/dist/contracts/required-file.js.map +1 -0
  59. package/dist/contracts/types.d.ts +61 -0
  60. package/dist/contracts/types.d.ts.map +1 -0
  61. package/dist/contracts/types.js +2 -0
  62. package/dist/contracts/types.js.map +1 -0
  63. package/dist/coverage/index.d.ts +55 -0
  64. package/dist/coverage/index.d.ts.map +1 -0
  65. package/dist/coverage/index.js +68 -0
  66. package/dist/coverage/index.js.map +1 -0
  67. package/dist/emit/collect.d.ts +20 -0
  68. package/dist/emit/collect.d.ts.map +1 -0
  69. package/dist/emit/collect.js +17 -0
  70. package/dist/emit/collect.js.map +1 -0
  71. package/dist/emit/header.d.ts +21 -0
  72. package/dist/emit/header.d.ts.map +1 -0
  73. package/dist/emit/header.js +20 -0
  74. package/dist/emit/header.js.map +1 -0
  75. package/dist/emit/index.d.ts +57 -0
  76. package/dist/emit/index.d.ts.map +1 -0
  77. package/dist/emit/index.js +128 -0
  78. package/dist/emit/index.js.map +1 -0
  79. package/dist/emit/merge.d.ts +23 -0
  80. package/dist/emit/merge.d.ts.map +1 -0
  81. package/dist/emit/merge.js +71 -0
  82. package/dist/emit/merge.js.map +1 -0
  83. package/dist/emit/write.d.ts +20 -0
  84. package/dist/emit/write.d.ts.map +1 -0
  85. package/dist/emit/write.js +61 -0
  86. package/dist/emit/write.js.map +1 -0
  87. package/dist/exceptions/index.d.ts +46 -0
  88. package/dist/exceptions/index.d.ts.map +1 -0
  89. package/dist/exceptions/index.js +117 -0
  90. package/dist/exceptions/index.js.map +1 -0
  91. package/dist/fs/index.d.ts +12 -0
  92. package/dist/fs/index.d.ts.map +1 -0
  93. package/dist/fs/index.js +78 -0
  94. package/dist/fs/index.js.map +1 -0
  95. package/dist/graph/cache.d.ts +12 -0
  96. package/dist/graph/cache.d.ts.map +1 -0
  97. package/dist/graph/cache.js +76 -0
  98. package/dist/graph/cache.js.map +1 -0
  99. package/dist/graph/edges.d.ts +16 -0
  100. package/dist/graph/edges.d.ts.map +1 -0
  101. package/dist/graph/edges.js +161 -0
  102. package/dist/graph/edges.js.map +1 -0
  103. package/dist/graph/index.d.ts +15 -0
  104. package/dist/graph/index.d.ts.map +1 -0
  105. package/dist/graph/index.js +90 -0
  106. package/dist/graph/index.js.map +1 -0
  107. package/dist/graph/match.d.ts +12 -0
  108. package/dist/graph/match.d.ts.map +1 -0
  109. package/dist/graph/match.js +35 -0
  110. package/dist/graph/match.js.map +1 -0
  111. package/dist/graph/program.d.ts +12 -0
  112. package/dist/graph/program.d.ts.map +1 -0
  113. package/dist/graph/program.js +54 -0
  114. package/dist/graph/program.js.map +1 -0
  115. package/dist/graph/types.d.ts +55 -0
  116. package/dist/graph/types.d.ts.map +1 -0
  117. package/dist/graph/types.js +2 -0
  118. package/dist/graph/types.js.map +1 -0
  119. package/dist/index.d.ts +21 -0
  120. package/dist/index.d.ts.map +1 -0
  121. package/dist/index.js +20 -0
  122. package/dist/index.js.map +1 -0
  123. package/dist/init/index.d.ts +23 -0
  124. package/dist/init/index.d.ts.map +1 -0
  125. package/dist/init/index.js +218 -0
  126. package/dist/init/index.js.map +1 -0
  127. package/dist/normalize/identity.d.ts +4 -0
  128. package/dist/normalize/identity.d.ts.map +1 -0
  129. package/dist/normalize/identity.js +33 -0
  130. package/dist/normalize/identity.js.map +1 -0
  131. package/dist/normalize/index.d.ts +3 -0
  132. package/dist/normalize/index.d.ts.map +1 -0
  133. package/dist/normalize/index.js +3 -0
  134. package/dist/normalize/index.js.map +1 -0
  135. package/dist/normalize/normalize.d.ts +34 -0
  136. package/dist/normalize/normalize.d.ts.map +1 -0
  137. package/dist/normalize/normalize.js +91 -0
  138. package/dist/normalize/normalize.js.map +1 -0
  139. package/dist/probes/graph.d.ts +19 -0
  140. package/dist/probes/graph.d.ts.map +1 -0
  141. package/dist/probes/graph.js +181 -0
  142. package/dist/probes/graph.js.map +1 -0
  143. package/dist/probes/index.d.ts +5 -0
  144. package/dist/probes/index.d.ts.map +1 -0
  145. package/dist/probes/index.js +5 -0
  146. package/dist/probes/index.js.map +1 -0
  147. package/dist/probes/lint.d.ts +17 -0
  148. package/dist/probes/lint.d.ts.map +1 -0
  149. package/dist/probes/lint.js +121 -0
  150. package/dist/probes/lint.js.map +1 -0
  151. package/dist/probes/model/anthropic.d.ts +47 -0
  152. package/dist/probes/model/anthropic.d.ts.map +1 -0
  153. package/dist/probes/model/anthropic.js +100 -0
  154. package/dist/probes/model/anthropic.js.map +1 -0
  155. package/dist/probes/model/budget.d.ts +22 -0
  156. package/dist/probes/model/budget.d.ts.map +1 -0
  157. package/dist/probes/model/budget.js +27 -0
  158. package/dist/probes/model/budget.js.map +1 -0
  159. package/dist/probes/model/cache.d.ts +13 -0
  160. package/dist/probes/model/cache.d.ts.map +1 -0
  161. package/dist/probes/model/cache.js +74 -0
  162. package/dist/probes/model/cache.js.map +1 -0
  163. package/dist/probes/model/index.d.ts +20 -0
  164. package/dist/probes/model/index.d.ts.map +1 -0
  165. package/dist/probes/model/index.js +160 -0
  166. package/dist/probes/model/index.js.map +1 -0
  167. package/dist/probes/model/prompt.d.ts +16 -0
  168. package/dist/probes/model/prompt.d.ts.map +1 -0
  169. package/dist/probes/model/prompt.js +57 -0
  170. package/dist/probes/model/prompt.js.map +1 -0
  171. package/dist/probes/model/provider.d.ts +88 -0
  172. package/dist/probes/model/provider.d.ts.map +1 -0
  173. package/dist/probes/model/provider.js +48 -0
  174. package/dist/probes/model/provider.js.map +1 -0
  175. package/dist/probes/structure.d.ts +3 -0
  176. package/dist/probes/structure.d.ts.map +1 -0
  177. package/dist/probes/structure.js +95 -0
  178. package/dist/probes/structure.js.map +1 -0
  179. package/dist/probes/types.d.ts +46 -0
  180. package/dist/probes/types.d.ts.map +1 -0
  181. package/dist/probes/types.js +15 -0
  182. package/dist/probes/types.js.map +1 -0
  183. package/dist/registry/define.d.ts +40 -0
  184. package/dist/registry/define.d.ts.map +1 -0
  185. package/dist/registry/define.js +4 -0
  186. package/dist/registry/define.js.map +1 -0
  187. package/dist/registry/ids.d.ts +19 -0
  188. package/dist/registry/ids.d.ts.map +1 -0
  189. package/dist/registry/ids.js +86 -0
  190. package/dist/registry/ids.js.map +1 -0
  191. package/dist/registry/index.d.ts +45 -0
  192. package/dist/registry/index.d.ts.map +1 -0
  193. package/dist/registry/index.js +71 -0
  194. package/dist/registry/index.js.map +1 -0
  195. package/dist/registry/load.d.ts +14 -0
  196. package/dist/registry/load.d.ts.map +1 -0
  197. package/dist/registry/load.js +108 -0
  198. package/dist/registry/load.js.map +1 -0
  199. package/dist/registry/validate.d.ts +24 -0
  200. package/dist/registry/validate.d.ts.map +1 -0
  201. package/dist/registry/validate.js +160 -0
  202. package/dist/registry/validate.js.map +1 -0
  203. package/dist/report/index.d.ts +10 -0
  204. package/dist/report/index.d.ts.map +1 -0
  205. package/dist/report/index.js +30 -0
  206. package/dist/report/index.js.map +1 -0
  207. package/dist/report/json.d.ts +20 -0
  208. package/dist/report/json.d.ts.map +1 -0
  209. package/dist/report/json.js +34 -0
  210. package/dist/report/json.js.map +1 -0
  211. package/dist/report/sarif.d.ts +21 -0
  212. package/dist/report/sarif.d.ts.map +1 -0
  213. package/dist/report/sarif.js +138 -0
  214. package/dist/report/sarif.js.map +1 -0
  215. package/dist/report/terminal.d.ts +3 -0
  216. package/dist/report/terminal.d.ts.map +1 -0
  217. package/dist/report/terminal.js +117 -0
  218. package/dist/report/terminal.js.map +1 -0
  219. package/dist/report/types.d.ts +28 -0
  220. package/dist/report/types.d.ts.map +1 -0
  221. package/dist/report/types.js +11 -0
  222. package/dist/report/types.js.map +1 -0
  223. package/dist/report/verdict.d.ts +21 -0
  224. package/dist/report/verdict.d.ts.map +1 -0
  225. package/dist/report/verdict.js +31 -0
  226. package/dist/report/verdict.js.map +1 -0
  227. package/dist/result.d.ts +22 -0
  228. package/dist/result.d.ts.map +1 -0
  229. package/dist/result.js +19 -0
  230. package/dist/result.js.map +1 -0
  231. package/dist/types.d.ts +178 -0
  232. package/dist/types.d.ts.map +1 -0
  233. package/dist/types.js +11 -0
  234. package/dist/types.js.map +1 -0
  235. package/dist/util/compilerApi.d.ts +2 -0
  236. package/dist/util/compilerApi.d.ts.map +1 -0
  237. package/dist/util/compilerApi.js +28 -0
  238. package/dist/util/compilerApi.js.map +1 -0
  239. package/dist/util/glob.d.ts +43 -0
  240. package/dist/util/glob.d.ts.map +1 -0
  241. package/dist/util/glob.js +195 -0
  242. package/dist/util/glob.js.map +1 -0
  243. package/dist/util/paths.d.ts +6 -0
  244. package/dist/util/paths.d.ts.map +1 -0
  245. package/dist/util/paths.js +22 -0
  246. package/dist/util/paths.js.map +1 -0
  247. package/dist/version.d.ts +13 -0
  248. package/dist/version.d.ts.map +1 -0
  249. package/dist/version.js +12 -0
  250. package/dist/version.js.map +1 -0
  251. package/package.json +72 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Mitesh Sharma
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,536 @@
1
+ # archcheck
2
+
3
+ **Write your architecture rules once. archcheck checks them, and generates the configuration that
4
+ enforces them.**
5
+
6
+ Your architecture lives in many places. It lives in a linter config, in a module-graph config, in a
7
+ CI script, and in a document that somebody wrote a year ago. Nothing keeps these in agreement. A
8
+ rule that changes in one place stays wrong in the others. The copy that people read is often the
9
+ copy that is wrong.
10
+
11
+ archcheck gives you one file. You declare each rule once. archcheck then does two things with it.
12
+
13
+ ```
14
+ architecture.config.ts
15
+ one file · one rule, once
16
+
17
+ ┌───────────────┴───────────────┐
18
+ │ │
19
+ GENERATE CHECK
20
+ │ │
21
+ generated/graph.config.cjs reads your code
22
+ generated/lint.overrides.json finds each violation
23
+ generated/required.json returns a typed finding
24
+ generated/rules.json │
25
+ the rule table in your docs ▼
26
+ │ ✗ domain may not import services
27
+ ▼ modules/ledger/domain/supersede.ts:4
28
+ your editor shows the rule → invert the call: the service
29
+ before you run anything calls the domain
30
+ ```
31
+
32
+ Every rule has one home. The generated files carry a header that says they are generated. A gate
33
+ fails if somebody edits one by hand.
34
+
35
+ ---
36
+
37
+ ## What you get
38
+
39
+ | | |
40
+ |---|---|
41
+ | **One source of truth** | You declare a rule once. Every mechanism reads it. |
42
+ | **Honest coverage** | archcheck counts your rules by tier and computes the percentage. You cannot write a better number by hand. |
43
+ | **A remedy in every finding** | Each finding says what to do next, not only what failed. |
44
+ | **An adoption path** | A baseline records the violations you already have. Your build stays green. You then fix them one at a time. |
45
+ | **Typed output for an agent** | `--format json` gives a stable, versioned shape. Exit codes tell an agent which file to fix. |
46
+ | **SARIF for CI** | `--format sarif` gives code scanning and your editor a format they already read. |
47
+ | **Zero runtime dependencies** | archcheck needs TypeScript, which you already have. |
48
+
49
+ ---
50
+
51
+ ## Install
52
+
53
+ You need **Node 20 or later**. You also need **TypeScript** in your project, in the range
54
+ `>=5.4 <7`. TypeScript 7 is the native port. It moved the compiler API that archcheck reads to
55
+ another subpath, so the range excludes it. archcheck names the installed version and the command to
56
+ fix it if it finds a TypeScript that does not expose that API.
57
+
58
+ ```bash
59
+ npm install --save-dev archcheck
60
+ ```
61
+
62
+ archcheck also runs with `bun`, `pnpm` and `yarn`.
63
+
64
+ ---
65
+
66
+ ## Quick start
67
+
68
+ Five steps. Each step is one command.
69
+
70
+ ### Step 1 — write a starting manifest
71
+
72
+ ```bash
73
+ npx archcheck init
74
+ ```
75
+
76
+ `init` reads your folder structure. It writes `architecture.config.ts`. Each folder under `src/`
77
+ becomes a layer. Each layer lists what it imports today.
78
+
79
+ Every generated rule has `severity: 'warn'`. Your build stays green, and archcheck exits `0`. This
80
+ is deliberate: a tool that blocks 400 times on the first run gets removed the same day.
81
+
82
+ archcheck still **reports** what it sees, and it marks each finding `(warn)`. A group heading says
83
+ how many findings block. Read them, and fix them in your own order.
84
+
85
+ ### Step 2 — look at what archcheck sees
86
+
87
+ ```bash
88
+ npx archcheck
89
+ ```
90
+
91
+ You get one group per tier, and the most important group comes first. Each finding says whether it
92
+ blocks.
93
+
94
+ ```
95
+ ✗ 2 deterministic (0 blocking)
96
+
97
+ services may not import repositories services-imports-only-what-it-declares (warn)
98
+ src/services/write.ts:3
99
+ allowed: contracts, domain
100
+ → declare the dependency, or invert the call
101
+
102
+ logging goes through one seam, never straight to the console no-console-outside-an-entry-point (warn)
103
+ src/entry/main.ts:9
104
+ → inject a logger, or list this path as an entry point in appliesTo
105
+
106
+ ◑ 0 proxy · the check is narrower than the rule
107
+ ○ 0 advisory
108
+ 8 rules · 7 deterministic · 1 proxy · 0 advisory
109
+ ```
110
+
111
+ Nothing here blocks yet. When a run has nothing to report at all, archcheck says `nothing to fix`.
112
+
113
+ ### Step 3 — tighten one rule
114
+
115
+ Open `architecture.config.ts`. Find a layer that imports something it must not. Remove that entry
116
+ from its `mayImport` list.
117
+
118
+ ```diff
119
+ params: {
120
+ name: 'services',
121
+ match: 'src/services/**',
122
+ - mayImport: ['contracts', 'domain', 'repositories'],
123
+ + mayImport: ['contracts', 'domain'],
124
+ },
125
+ ```
126
+
127
+ Run `npx archcheck` again. It now reports the imports that break the rule.
128
+
129
+ ```
130
+ ✗ 1 deterministic (0 blocking)
131
+
132
+ services may not import repositories services-imports-only-what-it-declares (warn)
133
+ src/services/write.ts:3
134
+ allowed: contracts, domain
135
+ → declare the dependency, or invert the call
136
+ ```
137
+
138
+ Fix the code. Then change that one rule to `severity: 'error'`.
139
+
140
+ ### Step 4 — generate the configuration
141
+
142
+ ```bash
143
+ npx archcheck emit
144
+ ```
145
+
146
+ archcheck writes the `generated/` folder. Commit it.
147
+
148
+ archcheck writes one file per **sink** your rules use, and `rules.json` always.
149
+
150
+ | File | The rules that write it |
151
+ |---|---|
152
+ | `generated/graph.config.cjs` | `layers`, `encapsulation` |
153
+ | `generated/lint.overrides.json` | `layers`, `naming`, `banned-syntax` |
154
+ | `generated/exports.json` | `encapsulation` |
155
+ | `generated/required.json` | `required-file`, `declared-field`, `judgement` |
156
+ | `generated/rules.json` | every rule — this is the data your documents read |
157
+
158
+ A sink with no rules writes no file. Your editor now shows each rule as you type, and it shows your
159
+ own sentence:
160
+
161
+ > `domain may not import services — invert the call: the service calls the domain`
162
+
163
+ ### Step 5 — add two scripts and one CI step
164
+
165
+ Add these lines to `package.json`:
166
+
167
+ ```json
168
+ {
169
+ "scripts": {
170
+ "arch": "archcheck",
171
+ "arch:emit": "archcheck emit --check"
172
+ }
173
+ }
174
+ ```
175
+
176
+ Add one step to your CI workflow:
177
+
178
+ ```yaml
179
+ - run: npx archcheck --format sarif > archcheck.sarif
180
+ - run: npx archcheck emit --check
181
+ ```
182
+
183
+ You are done. Repeat step 3 for each rule you want to enforce.
184
+
185
+ ---
186
+
187
+ ## A complete example
188
+
189
+ [`examples/minimal-service/`](examples/minimal-service/) is a small service with six layers. It has
190
+ a manifest, and it has one deliberate violation for each rule.
191
+
192
+ ```bash
193
+ cd examples/minimal-service
194
+ npx archcheck # green
195
+ npx archcheck coverage # 8 rules · 100% automatable
196
+ ```
197
+
198
+ Read [its README](examples/minimal-service/README.md). It explains each file, and it shows you how
199
+ to make the rules fail on purpose.
200
+
201
+ ---
202
+
203
+ ## How the rules work
204
+
205
+ You write each rule as one entry in the manifest. A rule has a **kind**. The kind decides what the
206
+ rule can say.
207
+
208
+ ```ts
209
+ import { defineArchitecture } from 'archcheck';
210
+
211
+ export default defineArchitecture({
212
+ rules: [
213
+ {
214
+ id: 'domain-decides-alone',
215
+ kind: 'layers',
216
+ statement: 'the domain reads contracts and rules',
217
+ severity: 'error',
218
+ confidence: 'deterministic',
219
+ params: {
220
+ name: 'domain',
221
+ match: 'src/domain/**',
222
+ mayImport: ['contracts', 'rules'],
223
+ catchesUnlayered: true,
224
+ },
225
+ remedy: { summary: 'invert the call: the service calls the domain' },
226
+ },
227
+ ],
228
+ });
229
+ ```
230
+
231
+ There are eight kinds. The set is closed.
232
+
233
+ | Kind | It says | Example |
234
+ |---|---|---|
235
+ | `layers` | which layers a layer may import | the domain must not import a service |
236
+ | `encapsulation` | a module is reached through its barrel | import `modules/ledger`, never `modules/ledger/services/write` |
237
+ | `naming` | what a name must look like | every hook starts with `use` |
238
+ | `banned-syntax` | a construct is forbidden here | a pure rule reads no clock |
239
+ | `required-file` | a file must exist | every port has a memory twin |
240
+ | `declared-field` | a call must declare a field | every endpoint declares a permission |
241
+ | `judgement` | a rule in plain English, judged by a model | a controller decides nothing |
242
+ | `forbidden` | one specific edge is banned | reserved; `layers` covers this today |
243
+
244
+ ### Default deny
245
+
246
+ A `layers` rule denies what it does not allow. A new folder that nobody declares fails on its first
247
+ commit. It is never silently legal.
248
+
249
+ One `layers` rule carries `catchesUnlayered: true`. That rule reports a file in no layer.
250
+
251
+ ### Three tiers, so archcheck does not pretend
252
+
253
+ Each rule declares its `confidence`. The tier says how much to trust a pass.
254
+
255
+ | Mark | Tier | What it means |
256
+ |---|---|---|
257
+ | ✓ | `deterministic` | A script decides. No judgement, and no model. |
258
+ | ◑ | `proxy` | The check catches the practical violations. The rule as written is broader. Treat a pass as evidence, not proof. |
259
+ | ○ | `model` | A model reads the code. The verdict is advisory. |
260
+
261
+ `npx archcheck coverage` counts your rules by tier. archcheck computes the percentage from the
262
+ manifest. Add a rule with no real check, and the percentage falls.
263
+
264
+ ---
265
+
266
+ ## Commands
267
+
268
+ ```
269
+ archcheck check everything
270
+ archcheck --changed check only what git says changed
271
+ archcheck --format json the shape an agent reads
272
+ archcheck --format sarif SARIF 2.1.0, for CI and your editor
273
+ archcheck --format verdict one answer, for a blocking hook
274
+ archcheck --only deterministic skip the proxy and model tiers
275
+
276
+ archcheck emit write the generated configuration
277
+ archcheck emit --check fail if the committed output is stale
278
+ archcheck explain <ruleId> the rule, the remedy, and its current violations
279
+ archcheck coverage the three counts
280
+ archcheck init write a starting manifest
281
+ archcheck baseline --write record today's violations as debt
282
+ archcheck baseline --prune drop entries that no longer reproduce
283
+ ```
284
+
285
+ ### Exit codes
286
+
287
+ An agent reads these. They are frozen.
288
+
289
+ | Code | Meaning | What to do |
290
+ |---|---|---|
291
+ | `0` | Clean. Or only baselined and advisory findings. | Continue. |
292
+ | `1` | New violations of an `error` rule. | Fix the code. |
293
+ | `2` | The configuration or the environment is wrong. | Read the error. Do **not** touch the code. |
294
+ | `3` | The generated output is stale. | Run `archcheck emit`. Write the document yourself if `docs` names one archcheck cannot read. |
295
+ | `4` | A recorded violation no longer happens. | Run `archcheck baseline --prune`. |
296
+
297
+ Code `2` is separate from code `1` on purpose. Code `1` says the code is wrong. Code `2` says the
298
+ code is not the problem. The cause is the manifest, another committed archcheck file, or the
299
+ environment archcheck runs in. An unknown layer name in `mayImport` and an expired exception are
300
+ manifest problems. A corrupt `.archcheck/ids.json`, a `--changed` run that git cannot answer, a
301
+ `baseline --prune` on a degraded run, and an `init` that cannot read the module graph are not.
302
+ archcheck names the real fix in the error text. An agent that reads that text does not spend its
303
+ turn on the wrong file.
304
+
305
+ ---
306
+
307
+ ## Adopt archcheck on a codebase that already breaks its rules
308
+
309
+ This is the normal case. You have two tools, and they do different jobs.
310
+
311
+ ```
312
+ a violation you already have a violation you choose to allow
313
+ │ │
314
+ ▼ ▼
315
+ archcheck baseline --write exceptions: [ … ] in the manifest
316
+ │ │
317
+ .archcheck/baseline.json owner + expiry + reason
318
+ │ │
319
+ DEBT POLICY
320
+ no owner · no expiry somebody signed for it
321
+ the file only shrinks it expires, and then it fails
322
+ ```
323
+
324
+ ### The baseline records debt
325
+
326
+ ```bash
327
+ npx archcheck baseline --write
328
+ ```
329
+
330
+ archcheck records each current violation by a **stable hash of the evidence**. The hash holds no
331
+ line number. Move the code, and the entry still matches. Your build goes green.
332
+
333
+ A new violation of the same rule still fails. The ratchet turns one way.
334
+
335
+ When you fix a recorded violation, archcheck exits `4`. Run `npx archcheck baseline --prune`. The
336
+ file shrinks. It cannot silently hide more than it should.
337
+
338
+ ### An exception records policy
339
+
340
+ ```ts
341
+ exceptions: [
342
+ {
343
+ rule: 'no-deep-module-imports',
344
+ path: 'modules/legacy/**',
345
+ owner: 'mitesh',
346
+ expires: '2026-12-31',
347
+ reason: 'legacy module, scheduled for removal in Q4',
348
+ },
349
+ ],
350
+ ```
351
+
352
+ An exception needs an owner, a date and a reason. archcheck reports every exception on every run,
353
+ and it reports the ones that expire within 30 days.
354
+
355
+ After the date, the run fails and names the owner. An exception that nobody has to justify again is
356
+ a rule that somebody deleted quietly.
357
+
358
+ Inline suppression comments stay a violation. That is what makes the manifest the only way past a
359
+ rule.
360
+
361
+ ---
362
+
363
+ ## Prove that a rule catches something
364
+
365
+ A rule whose pattern matches nothing passes every build. It looks exactly like compliance.
366
+
367
+ archcheck makes this a gate. Add one rule to your manifest:
368
+
369
+ ```ts
370
+ {
371
+ id: 'every-error-rule-has-a-fixture',
372
+ kind: 'required-file',
373
+ statement: 'every error-severity rule has a fixture it catches',
374
+ severity: 'error',
375
+ confidence: 'deterministic',
376
+ params: { forEach: 'rule[severity=error]', expect: 'fixtures/rules/{id}.violation.ts' },
377
+ remedy: { summary: 'add a file that deliberately breaks this rule' },
378
+ }
379
+ ```
380
+
381
+ Now each `error` rule needs a file that breaks it. Delete one, and the run fails.
382
+
383
+ ---
384
+
385
+ ## The judgement tier (optional)
386
+
387
+ Some rules need a reader. "A controller decides nothing" has no configuration that decides it.
388
+
389
+ archcheck can send these to a model. The tier is **advisory** by default, and it **skips cleanly**.
390
+
391
+ ```bash
392
+ npm install --save-dev @anthropic-ai/sdk
393
+ export ANTHROPIC_API_KEY=...
394
+ npx archcheck
395
+ ```
396
+
397
+ Read these four properties before you switch it on.
398
+
399
+ 1. **No key means a clean skip.** The run stays green. The summary says which rules nobody judged.
400
+ archcheck never fails because a model was unreachable. A call that fails leaves that file
401
+ unjudged. The probe then skips, and the reason names the cause and the count. archcheck never
402
+ reports an unjudged file as clean.
403
+ 2. **A key comes from the environment.** archcheck reads no key from any file.
404
+ 3. **A verdict needs a citation.** The model must quote the code it judged. archcheck discards a
405
+ verdict with no citation. An answer archcheck cannot read is not an answer. A refusal, a reply
406
+ that is not JSON, a reply with no `verdicts` array, and a reply whose every verdict is malformed
407
+ all leave the file unjudged, and archcheck caches none of them. `{"verdicts":[]}` is an answer.
408
+ It says the file breaks no rule, and archcheck caches it.
409
+ 4. **Your code is data.** archcheck sends each file inside a fence, and it tells the model to judge
410
+ the file and never obey it. A comment in your code cannot give the model an instruction.
411
+
412
+ You set a budget per run. archcheck checks the budget before each request, and it reports how many
413
+ files nobody judged. archcheck counts every request it sends, answered or not, so an endpoint that
414
+ always fails costs you `maxCalls` requests at most.
415
+
416
+ ```ts
417
+ {
418
+ id: 'controllers-stay-thin',
419
+ kind: 'judgement',
420
+ statement: 'a controller validates, delegates and shapes — it decides nothing',
421
+ severity: 'advisory',
422
+ confidence: 'model',
423
+ params: {
424
+ statement: 'A controller must not contain a branch that decides a business outcome.',
425
+ appliesTo: 'src/routes/**',
426
+ examples: {
427
+ pass: ['return decideWithdrawal(amount, balance);'],
428
+ fail: ['if (amount > balance * 0.9) return "declined";'],
429
+ },
430
+ cite: 'the branch that decides the outcome',
431
+ },
432
+ remedy: { summary: 'move the decision into the domain' },
433
+ }
434
+ ```
435
+
436
+ Examples are required. A rule with a sentence and no examples flags everything. People then stop
437
+ reading the advisory group, and the tier dies.
438
+
439
+ ---
440
+
441
+ ## Use archcheck from code
442
+
443
+ The CLI owns no behaviour. Every command is one method.
444
+
445
+ ```ts
446
+ import { load, exitCodeFor } from 'archcheck';
447
+
448
+ const loaded = load({ cwd: process.cwd() });
449
+ if (!loaded.ok) throw new Error(loaded.error.join('\n'));
450
+
451
+ const run = await loaded.value.check({ changed: ['src/domain/decide.ts'] });
452
+ if (run.ok) {
453
+ for (const finding of run.value.diagnostics) {
454
+ console.log(finding.statement, finding.file, finding.remedy.summary);
455
+ }
456
+ process.exit(exitCodeFor(run.value));
457
+ }
458
+ ```
459
+
460
+ An agent hook does not shell out. Every result is typed, and no call throws.
461
+
462
+ Several violations of one rule in one file collapse into one finding. The `occurrences` field says
463
+ how many violations collapsed into it. The `occurrenceLines` field says which lines they are on. The
464
+ baseline records the count, so a new occurrence in a file the baseline already covers still fails.
465
+ The hash does not change: the evidence holds no line number, and the count stays out of the
466
+ evidence. No baseline file you already have becomes invalid.
467
+
468
+ ---
469
+
470
+ ## How archcheck reads your code
471
+
472
+ archcheck owns its module graph. It builds one program with the TypeScript compiler, and every
473
+ check reads it.
474
+
475
+ ```
476
+ your tsconfig.json
477
+
478
+
479
+ ┌───────────────┐ one program, one parse, shared by every check
480
+ │ module graph │ every import edge · type-only · dynamic · computed
481
+ └───────┬───────┘
482
+
483
+ ┌──────┼──────────┬───────────────┐
484
+ ▼ ▼ ▼ ▼
485
+ graph lint structure judgement
486
+ layers naming required-file plain English
487
+ barrels bans declared-field (advisory)
488
+ │ │ │ │
489
+ └──────┴────┬─────┴───────────────┘
490
+
491
+ one finding per violation
492
+ + a stable hash of the evidence
493
+
494
+ terminal · json · sarif · verdict
495
+ ```
496
+
497
+ archcheck owns the graph for three reasons.
498
+
499
+ 1. **A stable hash.** The hash comes from the evidence, and never from a line number. Move a
500
+ function, and the baseline still matches.
501
+ 2. **A computed import stays visible.** `import(\`./${name}.js\`)` resolves to nothing. A graph tool
502
+ cannot see it, and a layer violation hides there. archcheck reports it.
503
+ 3. **One parse.** Every check reads the same program. archcheck does not run your linter again.
504
+
505
+ ---
506
+
507
+ ## Contributing
508
+
509
+ archcheck checks itself. Its own manifest is [`architecture.config.ts`](architecture.config.ts), and
510
+ its own rule table is [`RULES.md`](RULES.md).
511
+
512
+ ```bash
513
+ bun install
514
+ bun run check
515
+ ```
516
+
517
+ `bun run check` runs every gate. The pre-push hook and CI run the same scripts.
518
+
519
+ | Gate | It enforces |
520
+ |---|---|
521
+ | `typecheck` | no type errors, strict |
522
+ | `lint` | format, style, and no Bun-only API in `src/` |
523
+ | `tests` | every test, co-located beside its subject |
524
+ | `emit` | the committed generated output is not stale |
525
+ | `selfhost` | archcheck passes its own rules |
526
+
527
+ Two standing rules for a change:
528
+
529
+ 1. A bug fix lands its reproducing test first.
530
+ 2. Every `error` rule has a fixture that it catches.
531
+
532
+ ---
533
+
534
+ ## License
535
+
536
+ MIT. See [LICENSE](LICENSE).
@@ -0,0 +1,99 @@
1
+ import type { StaleFile } from './emit/write.js';
2
+ import { type Registry } from './registry/index.js';
3
+ import { type Result } from './result.js';
4
+ import type { Confidence, ContractKind, Coverage, Diagnostic, RunResult } from './types.js';
5
+ /** Exit codes are an API. An agent branches on them, so they are frozen from v0.1 (LLD §6). */
6
+ export declare const EXIT: {
7
+ /** Clean, or only baselined and advisory findings. */
8
+ readonly clean: 0;
9
+ /** New violations of error-severity rules. Fix the code. */
10
+ readonly violations: 1;
11
+ /** Configuration invalid. Fix the manifest, NOT the code. */
12
+ readonly config: 2;
13
+ /** Emitted output is stale. Run `archcheck emit`. */
14
+ readonly stale: 3;
15
+ /** A baselined finding no longer reproduces. Run `archcheck baseline --prune` (plan 14). */
16
+ readonly baselineStale: 4;
17
+ };
18
+ export type ExitCode = (typeof EXIT)[keyof typeof EXIT];
19
+ export type CheckOptions = {
20
+ /** Repo-relative POSIX paths. Undefined means the whole repository. */
21
+ changed?: readonly string[];
22
+ /** Run only rules at or above this confidence tier. `deterministic` skips the model tier. */
23
+ only?: Confidence;
24
+ /**
25
+ * Identities the baseline records. Omitted means "read `.archcheck/baseline.json`", which is
26
+ * what every real caller wants; pass an explicit set only to test without a file.
27
+ */
28
+ baselined?: ReadonlySet<string>;
29
+ /** Override today's date. Only the expiry-boundary tests have a reason to. */
30
+ today?: string;
31
+ /** Ingest `.archcheck/graph.json` instead of reading the graph ourselves. */
32
+ ingestGraph?: boolean;
33
+ /**
34
+ * Write `.archcheck/graph.json` as a side effect of this run. The other half of `ingestGraph`:
35
+ * CI builds the graph once, writes it here, and every later gate ingests it rather than paying
36
+ * for a second parse (LLD §1.3).
37
+ */
38
+ writeGraph?: boolean;
39
+ cache?: boolean;
40
+ };
41
+ export type EmitResult = {
42
+ written: {
43
+ path: string;
44
+ changed: boolean;
45
+ }[];
46
+ ledgerPath: string;
47
+ /**
48
+ * Rule kinds this version generated no configuration for. Carried here because `emit` has no
49
+ * `skipped` channel, so a programmatic caller has nowhere else to learn it — the CLI says the
50
+ * same thing through `describeUnimplemented` (src/emit/index.ts), which also has to serve the
51
+ * `--check` path, where there is no `EmitResult`.
52
+ */
53
+ unimplemented: ContractKind[];
54
+ };
55
+ export type BaselineOutcome = {
56
+ path: string;
57
+ entries: number;
58
+ removed: number;
59
+ /**
60
+ * Entries that survived with a LOWER recorded count than before. `--prune` tightens the ratchet
61
+ * two ways, and only `removed` was visible: a prune that fixed one of two occurrences reported
62
+ * "pruned 0 entries" and read as having done nothing. Pinned by 'a prune that only reduced a
63
+ * count says so, rather than reporting nothing' in tests/adoption.test.ts.
64
+ */
65
+ reduced: number;
66
+ };
67
+ export type ArchCheck = {
68
+ readonly registry: Registry;
69
+ check(options?: CheckOptions): Promise<Result<RunResult>>;
70
+ /** `baseline --write` / `baseline --prune`. Both go through a real run first. */
71
+ baseline(mode: 'write' | 'prune'): Promise<Result<BaselineOutcome>>;
72
+ emit(): Result<EmitResult>;
73
+ emitCheck(): Result<StaleFile[]>;
74
+ /** Statement, rationale, allowed alternatives and CURRENT violations. */
75
+ explain(ruleId: string): Promise<Result<string>>;
76
+ coverage(): Coverage;
77
+ };
78
+ export type LoadOptions = {
79
+ cwd?: string;
80
+ configPath?: string;
81
+ skipIdCheck?: boolean;
82
+ };
83
+ export declare function load(options?: LoadOptions): Result<ArchCheck>;
84
+ /**
85
+ * What `explain` appends once the run has happened — the live violation count. It counts
86
+ * OCCURRENCES, not diagnostics: an agent asking "how many" means how many places to fix, and one
87
+ * diagnostic can stand for several (see `Diagnostic.occurrences`).
88
+ */
89
+ export declare function describeViolations(ruleId: string, diagnostics: readonly Diagnostic[]): string;
90
+ /**
91
+ * The order matters. An expired exception is a MANIFEST problem (exit 2) and is checked first,
92
+ * because the violations it stopped covering are a consequence of it rather than news of their own.
93
+ * A stale baseline (exit 4) outranks live violations for the same reason: the fix is
94
+ * `baseline --prune`, and reporting exit 1 would send an agent to edit code that is already right.
95
+ */
96
+ export { buildInitManifest, inferLayers, initSummary, renderManifest, } from './init/index.js';
97
+ export { CONFIG_FILENAME } from './registry/index.js';
98
+ export declare function exitCodeFor(result: RunResult): ExitCode;
99
+ //# sourceMappingURL=archcheck.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"archcheck.d.ts","sourceRoot":"","sources":["../src/archcheck.ts"],"names":[],"mappings":"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"}