@shivaedev/quality 0.4.0 → 0.5.1

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 (387) hide show
  1. package/CHANGELOG.md +32 -0
  2. package/README.md +119 -29
  3. package/biome/declarations.json +17 -0
  4. package/biome/plugins/ambient-runtime.grit +18 -0
  5. package/biome/plugins/effect-fn-spans.grit +7 -0
  6. package/biome/preset.json +602 -0
  7. package/dist/baseline/compare.d.ts +8 -9
  8. package/dist/baseline/compare.d.ts.map +1 -1
  9. package/dist/baseline/compare.js +4 -4
  10. package/dist/baseline/compare.js.map +1 -1
  11. package/dist/baseline/format.d.ts +4 -4
  12. package/dist/baseline/format.d.ts.map +1 -1
  13. package/dist/baseline/format.js +3 -2
  14. package/dist/baseline/format.js.map +1 -1
  15. package/dist/baseline/guard.d.ts +3 -3
  16. package/dist/baseline/guard.d.ts.map +1 -1
  17. package/dist/baseline/guard.js +6 -5
  18. package/dist/baseline/guard.js.map +1 -1
  19. package/dist/baseline/legacy.d.ts +3 -3
  20. package/dist/baseline/legacy.d.ts.map +1 -1
  21. package/dist/baseline/legacy.js.map +1 -1
  22. package/dist/baseline/rewrite.d.ts +1 -1
  23. package/dist/baseline/rewrite.d.ts.map +1 -1
  24. package/dist/baseline/rewrite.js.map +1 -1
  25. package/dist/baseline/update.d.ts +8 -9
  26. package/dist/baseline/update.d.ts.map +1 -1
  27. package/dist/baseline/update.js +10 -9
  28. package/dist/baseline/update.js.map +1 -1
  29. package/dist/biome/report.d.ts +33 -0
  30. package/dist/biome/report.d.ts.map +1 -0
  31. package/dist/biome/report.js +25 -0
  32. package/dist/biome/report.js.map +1 -0
  33. package/dist/biome/run.d.ts +7 -0
  34. package/dist/biome/run.d.ts.map +1 -0
  35. package/dist/biome/run.js +16 -0
  36. package/dist/biome/run.js.map +1 -0
  37. package/dist/cli/args.d.ts +7 -3
  38. package/dist/cli/args.d.ts.map +1 -1
  39. package/dist/cli/args.js +12 -5
  40. package/dist/cli/args.js.map +1 -1
  41. package/dist/cli/baseline.d.ts +1 -1
  42. package/dist/cli/baseline.d.ts.map +1 -1
  43. package/dist/cli/baseline.js +3 -3
  44. package/dist/cli/baseline.js.map +1 -1
  45. package/dist/cli/check.js.map +1 -1
  46. package/dist/cli/fix.d.ts +4 -0
  47. package/dist/cli/fix.d.ts.map +1 -0
  48. package/dist/cli/fix.js +21 -0
  49. package/dist/cli/fix.js.map +1 -0
  50. package/dist/cli/lint.js +1 -1
  51. package/dist/cli/lint.js.map +1 -1
  52. package/dist/cli/run-main.d.ts.map +1 -1
  53. package/dist/cli/run-main.js +7 -3
  54. package/dist/cli/run-main.js.map +1 -1
  55. package/dist/cli.js +3 -0
  56. package/dist/cli.js.map +1 -1
  57. package/dist/config/decode.d.ts +4 -4
  58. package/dist/config/decode.js +12 -12
  59. package/dist/config/decode.js.map +1 -1
  60. package/dist/config/load.d.ts +3 -3
  61. package/dist/config/load.d.ts.map +1 -1
  62. package/dist/config/load.js +1 -1
  63. package/dist/config/load.js.map +1 -1
  64. package/dist/config/resolve.d.ts +3 -5
  65. package/dist/config/resolve.d.ts.map +1 -1
  66. package/dist/config/resolve.js +11 -5
  67. package/dist/config/resolve.js.map +1 -1
  68. package/dist/config.d.ts +9 -9
  69. package/dist/config.d.ts.map +1 -1
  70. package/dist/config.js.map +1 -1
  71. package/dist/decoded.d.ts +1 -1
  72. package/dist/decoded.d.ts.map +1 -1
  73. package/dist/engine/baseline-file.d.ts +1 -1
  74. package/dist/engine/baseline-file.d.ts.map +1 -1
  75. package/dist/engine/evaluate.d.ts +8 -9
  76. package/dist/engine/evaluate.d.ts.map +1 -1
  77. package/dist/engine/evaluate.js +4 -4
  78. package/dist/engine/evaluate.js.map +1 -1
  79. package/dist/engine/run-rules.d.ts +4 -3
  80. package/dist/engine/run-rules.d.ts.map +1 -1
  81. package/dist/engine/run-rules.js +3 -3
  82. package/dist/engine/run-rules.js.map +1 -1
  83. package/dist/engine/session.d.ts +3 -3
  84. package/dist/engine/session.d.ts.map +1 -1
  85. package/dist/engine/violation.d.ts +11 -3
  86. package/dist/engine/violation.d.ts.map +1 -1
  87. package/dist/engine/violation.js +6 -2
  88. package/dist/engine/violation.js.map +1 -1
  89. package/dist/exceptions/registry.d.ts +7 -8
  90. package/dist/exceptions/registry.d.ts.map +1 -1
  91. package/dist/exceptions/registry.js +6 -6
  92. package/dist/exceptions/registry.js.map +1 -1
  93. package/dist/git/base.d.ts +1 -1
  94. package/dist/git/base.d.ts.map +1 -1
  95. package/dist/git/base.js.map +1 -1
  96. package/dist/git/command.d.ts +3 -3
  97. package/dist/git/command.d.ts.map +1 -1
  98. package/dist/git/command.js.map +1 -1
  99. package/dist/git/history.d.ts +1 -1
  100. package/dist/git/history.d.ts.map +1 -1
  101. package/dist/git/history.js +1 -1
  102. package/dist/git/history.js.map +1 -1
  103. package/dist/imports/ambient.d.ts +7 -0
  104. package/dist/imports/ambient.d.ts.map +1 -0
  105. package/dist/imports/ambient.js +44 -0
  106. package/dist/imports/ambient.js.map +1 -0
  107. package/dist/imports/components.d.ts +2 -0
  108. package/dist/imports/components.d.ts.map +1 -0
  109. package/dist/imports/components.js +63 -0
  110. package/dist/imports/components.js.map +1 -0
  111. package/dist/imports/extract.d.ts +10 -0
  112. package/dist/imports/extract.d.ts.map +1 -0
  113. package/dist/imports/extract.js +100 -0
  114. package/dist/imports/extract.js.map +1 -0
  115. package/dist/imports/fences/dsl.d.ts +18 -0
  116. package/dist/imports/fences/dsl.d.ts.map +1 -0
  117. package/dist/imports/fences/dsl.js +14 -0
  118. package/dist/imports/fences/dsl.js.map +1 -0
  119. package/dist/imports/fences/evaluate.d.ts +12 -0
  120. package/dist/imports/fences/evaluate.d.ts.map +1 -0
  121. package/dist/imports/fences/evaluate.js +55 -0
  122. package/dist/imports/fences/evaluate.js.map +1 -0
  123. package/dist/imports/fences/match.d.ts +17 -0
  124. package/dist/imports/fences/match.d.ts.map +1 -0
  125. package/dist/imports/fences/match.js +106 -0
  126. package/dist/imports/fences/match.js.map +1 -0
  127. package/dist/imports/fences/model.d.ts +60 -0
  128. package/dist/imports/fences/model.d.ts.map +1 -0
  129. package/dist/imports/fences/model.js +2 -0
  130. package/dist/imports/fences/model.js.map +1 -0
  131. package/dist/imports/fences/policy.d.ts +5 -0
  132. package/dist/imports/fences/policy.d.ts.map +1 -0
  133. package/dist/imports/fences/policy.js +59 -0
  134. package/dist/imports/fences/policy.js.map +1 -0
  135. package/dist/imports/fences/reach.d.ts +12 -0
  136. package/dist/imports/fences/reach.d.ts.map +1 -0
  137. package/dist/imports/fences/reach.js +39 -0
  138. package/dist/imports/fences/reach.js.map +1 -0
  139. package/dist/imports/fences/selectors.d.ts +11 -0
  140. package/dist/imports/fences/selectors.d.ts.map +1 -0
  141. package/dist/imports/fences/selectors.js +27 -0
  142. package/dist/imports/fences/selectors.js.map +1 -0
  143. package/dist/imports/fences/vocabulary.d.ts +9 -0
  144. package/dist/imports/fences/vocabulary.d.ts.map +1 -0
  145. package/dist/imports/fences/vocabulary.js +40 -0
  146. package/dist/imports/fences/vocabulary.js.map +1 -0
  147. package/dist/imports/generated.d.ts +6 -0
  148. package/dist/imports/generated.d.ts.map +1 -0
  149. package/dist/imports/generated.js +78 -0
  150. package/dist/imports/generated.js.map +1 -0
  151. package/dist/imports/glob.d.ts +2 -0
  152. package/dist/imports/glob.d.ts.map +1 -0
  153. package/dist/imports/glob.js +125 -0
  154. package/dist/imports/glob.js.map +1 -0
  155. package/dist/imports/graph.d.ts +23 -0
  156. package/dist/imports/graph.d.ts.map +1 -0
  157. package/dist/imports/graph.js +56 -0
  158. package/dist/imports/graph.js.map +1 -0
  159. package/dist/imports/paths.d.ts +2 -0
  160. package/dist/imports/paths.d.ts.map +1 -0
  161. package/dist/imports/paths.js +24 -0
  162. package/dist/imports/paths.js.map +1 -0
  163. package/dist/imports/pnpm-workspace.d.ts +2 -0
  164. package/dist/imports/pnpm-workspace.d.ts.map +1 -0
  165. package/dist/imports/pnpm-workspace.js +122 -0
  166. package/dist/imports/pnpm-workspace.js.map +1 -0
  167. package/dist/imports/projects.d.ts +10 -0
  168. package/dist/imports/projects.d.ts.map +1 -0
  169. package/dist/imports/projects.js +83 -0
  170. package/dist/imports/projects.js.map +1 -0
  171. package/dist/imports/resolve.d.ts +16 -0
  172. package/dist/imports/resolve.d.ts.map +1 -0
  173. package/dist/imports/resolve.js +88 -0
  174. package/dist/imports/resolve.js.map +1 -0
  175. package/dist/imports/workspace.d.ts +10 -0
  176. package/dist/imports/workspace.d.ts.map +1 -0
  177. package/dist/imports/workspace.js +55 -0
  178. package/dist/imports/workspace.js.map +1 -0
  179. package/dist/index.d.ts +3 -0
  180. package/dist/index.d.ts.map +1 -1
  181. package/dist/index.js +2 -0
  182. package/dist/index.js.map +1 -1
  183. package/dist/inventory/collect.d.ts +6 -6
  184. package/dist/inventory/collect.d.ts.map +1 -1
  185. package/dist/inventory/collect.js.map +1 -1
  186. package/dist/manifests/sorted.d.ts +14 -0
  187. package/dist/manifests/sorted.d.ts.map +1 -0
  188. package/dist/manifests/sorted.js +39 -0
  189. package/dist/manifests/sorted.js.map +1 -0
  190. package/dist/report/guard.d.ts +1 -1
  191. package/dist/report/guard.d.ts.map +1 -1
  192. package/dist/report/guard.js +1 -1
  193. package/dist/report/guard.js.map +1 -1
  194. package/dist/report/render.d.ts +2 -2
  195. package/dist/report/render.d.ts.map +1 -1
  196. package/dist/report/render.js.map +1 -1
  197. package/dist/report/stale.d.ts +2 -2
  198. package/dist/report/stale.d.ts.map +1 -1
  199. package/dist/report/stale.js.map +1 -1
  200. package/dist/rule.d.ts +12 -11
  201. package/dist/rule.d.ts.map +1 -1
  202. package/dist/rule.js.map +1 -1
  203. package/dist/rules/biome.d.ts +4 -0
  204. package/dist/rules/biome.d.ts.map +1 -0
  205. package/dist/rules/biome.js +40 -0
  206. package/dist/rules/biome.js.map +1 -0
  207. package/dist/rules/built-in.d.ts +8 -10
  208. package/dist/rules/built-in.d.ts.map +1 -1
  209. package/dist/rules/built-in.js +12 -0
  210. package/dist/rules/built-in.js.map +1 -1
  211. package/dist/rules/comments/kinds.d.ts +1 -2
  212. package/dist/rules/comments/kinds.d.ts.map +1 -1
  213. package/dist/rules/comments/kinds.js +7 -16
  214. package/dist/rules/comments/kinds.js.map +1 -1
  215. package/dist/rules/comments/max-per-file.d.ts +1 -1
  216. package/dist/rules/comments/max-per-file.js +11 -11
  217. package/dist/rules/comments/max-per-file.js.map +1 -1
  218. package/dist/rules/comments/no-banner.js +2 -2
  219. package/dist/rules/comments/no-banner.js.map +1 -1
  220. package/dist/rules/comments/no-environment-pragma.d.ts +2 -0
  221. package/dist/rules/comments/no-environment-pragma.d.ts.map +1 -0
  222. package/dist/rules/comments/no-environment-pragma.js +8 -0
  223. package/dist/rules/comments/no-environment-pragma.js.map +1 -0
  224. package/dist/rules/comments/no-jsdoc.js +3 -3
  225. package/dist/rules/comments/no-jsdoc.js.map +1 -1
  226. package/dist/rules/comments/no-line-reference.js +2 -2
  227. package/dist/rules/comments/no-line-reference.js.map +1 -1
  228. package/dist/rules/comments/no-pr-reference.js +6 -6
  229. package/dist/rules/comments/no-pr-reference.js.map +1 -1
  230. package/dist/rules/comments/no-todo.js +2 -2
  231. package/dist/rules/comments/no-todo.js.map +1 -1
  232. package/dist/rules/comments/pattern-rule.d.ts +2 -2
  233. package/dist/rules/comments/pattern-rule.d.ts.map +1 -1
  234. package/dist/rules/comments/pattern-rule.js +2 -2
  235. package/dist/rules/comments/pattern-rule.js.map +1 -1
  236. package/dist/rules/comments/scan.d.ts +6 -6
  237. package/dist/rules/comments/scan.d.ts.map +1 -1
  238. package/dist/rules/comments/scan.js +2 -2
  239. package/dist/rules/comments/scan.js.map +1 -1
  240. package/dist/rules/imports/cycles.d.ts +2 -0
  241. package/dist/rules/imports/cycles.d.ts.map +1 -0
  242. package/dist/rules/imports/cycles.js +38 -0
  243. package/dist/rules/imports/cycles.js.map +1 -0
  244. package/dist/rules/imports/fences.d.ts +5 -0
  245. package/dist/rules/imports/fences.d.ts.map +1 -0
  246. package/dist/rules/imports/fences.js +24 -0
  247. package/dist/rules/imports/fences.js.map +1 -0
  248. package/dist/rules/imports/resolvable.d.ts +4 -0
  249. package/dist/rules/imports/resolvable.d.ts.map +1 -0
  250. package/dist/rules/imports/resolvable.js +22 -0
  251. package/dist/rules/imports/resolvable.js.map +1 -0
  252. package/dist/rules/manifests-sorted.d.ts +2 -0
  253. package/dist/rules/manifests-sorted.d.ts.map +1 -0
  254. package/dist/rules/manifests-sorted.js +22 -0
  255. package/dist/rules/manifests-sorted.js.map +1 -0
  256. package/dist/rules/max-lines.js +4 -4
  257. package/dist/rules/max-lines.js.map +1 -1
  258. package/dist/rules/suppressions/biome/configs.d.ts +6 -6
  259. package/dist/rules/suppressions/biome/configs.d.ts.map +1 -1
  260. package/dist/rules/suppressions/biome/configs.js +2 -2
  261. package/dist/rules/suppressions/biome/configs.js.map +1 -1
  262. package/dist/rules/suppressions/biome/declarations.d.ts +4 -4
  263. package/dist/rules/suppressions/biome/declarations.d.ts.map +1 -1
  264. package/dist/rules/suppressions/biome/declarations.js +3 -3
  265. package/dist/rules/suppressions/biome/declarations.js.map +1 -1
  266. package/dist/rules/suppressions/biome/json.d.ts +2 -2
  267. package/dist/rules/suppressions/biome/json.d.ts.map +1 -1
  268. package/dist/rules/suppressions/biome/json.js +1 -1
  269. package/dist/rules/suppressions/biome/json.js.map +1 -1
  270. package/dist/rules/suppressions/biome/resolve.js +3 -3
  271. package/dist/rules/suppressions/biome/resolve.js.map +1 -1
  272. package/dist/rules/suppressions/biome/weakenings.d.ts +4 -4
  273. package/dist/rules/suppressions/biome/weakenings.d.ts.map +1 -1
  274. package/dist/rules/suppressions/biome/weakenings.js.map +1 -1
  275. package/dist/rules/suppressions/biome-overrides.d.ts +1 -1
  276. package/dist/rules/suppressions/biome-overrides.js +4 -4
  277. package/dist/rules/suppressions/biome-overrides.js.map +1 -1
  278. package/dist/rules/suppressions/directives.d.ts +1 -1
  279. package/dist/rules/suppressions/directives.d.ts.map +1 -1
  280. package/dist/rules/suppressions/directives.js +11 -11
  281. package/dist/rules/suppressions/directives.js.map +1 -1
  282. package/dist/rules/suppressions/no-double-cast.js +2 -2
  283. package/dist/rules/suppressions/no-double-cast.js.map +1 -1
  284. package/dist/rules/suppressions/no-inline.d.ts +1 -7
  285. package/dist/rules/suppressions/no-inline.d.ts.map +1 -1
  286. package/dist/rules/suppressions/no-inline.js +7 -30
  287. package/dist/rules/suppressions/no-inline.js.map +1 -1
  288. package/dist/rules/suppressions/stylesheet-comments.d.ts +1 -1
  289. package/dist/rules/suppressions/stylesheet-comments.d.ts.map +1 -1
  290. package/dist/rules/suppressions/stylesheet-comments.js +3 -3
  291. package/dist/rules/suppressions/stylesheet-comments.js.map +1 -1
  292. package/dist/rules/syntax.js +4 -4
  293. package/dist/rules/syntax.js.map +1 -1
  294. package/dist/standard-schema.d.ts +1 -1
  295. package/dist/standard-schema.d.ts.map +1 -1
  296. package/dist/vitest.d.ts +3 -0
  297. package/dist/vitest.d.ts.map +1 -0
  298. package/dist/vitest.js +20 -0
  299. package/dist/vitest.js.map +1 -0
  300. package/package.json +46 -31
  301. package/src/baseline/compare.ts +10 -15
  302. package/src/baseline/format.ts +8 -6
  303. package/src/baseline/guard.ts +11 -10
  304. package/src/baseline/legacy.ts +3 -3
  305. package/src/baseline/rewrite.ts +1 -1
  306. package/src/baseline/update.ts +21 -21
  307. package/src/biome/report.ts +35 -0
  308. package/src/biome/run.ts +23 -0
  309. package/src/cli/args.ts +16 -8
  310. package/src/cli/baseline.ts +5 -6
  311. package/src/cli/check.ts +1 -1
  312. package/src/cli/fix.ts +23 -0
  313. package/src/cli/lint.ts +1 -1
  314. package/src/cli/run-main.ts +8 -3
  315. package/src/cli.ts +3 -0
  316. package/src/config/decode.ts +12 -12
  317. package/src/config/load.ts +4 -4
  318. package/src/config/resolve.ts +15 -11
  319. package/src/config.ts +9 -9
  320. package/src/decoded.ts +1 -1
  321. package/src/engine/baseline-file.ts +1 -1
  322. package/src/engine/evaluate.ts +14 -16
  323. package/src/engine/run-rules.ts +8 -7
  324. package/src/engine/session.ts +3 -3
  325. package/src/engine/violation.ts +18 -4
  326. package/src/exceptions/registry.ts +10 -16
  327. package/src/git/base.ts +2 -2
  328. package/src/git/command.ts +3 -3
  329. package/src/git/history.ts +2 -2
  330. package/src/imports/ambient.ts +54 -0
  331. package/src/imports/components.ts +80 -0
  332. package/src/imports/extract.ts +129 -0
  333. package/src/imports/fences/dsl.ts +33 -0
  334. package/src/imports/fences/evaluate.ts +75 -0
  335. package/src/imports/fences/match.ts +148 -0
  336. package/src/imports/fences/model.ts +42 -0
  337. package/src/imports/fences/policy.ts +70 -0
  338. package/src/imports/fences/reach.ts +62 -0
  339. package/src/imports/fences/selectors.ts +37 -0
  340. package/src/imports/fences/vocabulary.ts +51 -0
  341. package/src/imports/generated.ts +95 -0
  342. package/src/imports/glob.ts +142 -0
  343. package/src/imports/graph.ts +81 -0
  344. package/src/imports/paths.ts +28 -0
  345. package/src/imports/pnpm-workspace.ts +134 -0
  346. package/src/imports/projects.ts +106 -0
  347. package/src/imports/resolve.ts +115 -0
  348. package/src/imports/workspace.ts +76 -0
  349. package/src/index.ts +3 -0
  350. package/src/inventory/collect.ts +6 -6
  351. package/src/manifests/sorted.ts +46 -0
  352. package/src/report/guard.ts +2 -2
  353. package/src/report/render.ts +9 -9
  354. package/src/report/stale.ts +3 -3
  355. package/src/rule.ts +13 -12
  356. package/src/rules/biome.ts +48 -0
  357. package/src/rules/built-in.ts +12 -0
  358. package/src/rules/comments/kinds.ts +11 -21
  359. package/src/rules/comments/max-per-file.ts +13 -13
  360. package/src/rules/comments/no-banner.ts +2 -2
  361. package/src/rules/comments/no-environment-pragma.ts +9 -0
  362. package/src/rules/comments/no-jsdoc.ts +3 -3
  363. package/src/rules/comments/no-line-reference.ts +2 -2
  364. package/src/rules/comments/no-pr-reference.ts +6 -6
  365. package/src/rules/comments/no-todo.ts +2 -2
  366. package/src/rules/comments/pattern-rule.ts +6 -6
  367. package/src/rules/comments/scan.ts +13 -13
  368. package/src/rules/imports/cycles.ts +44 -0
  369. package/src/rules/imports/fences.ts +27 -0
  370. package/src/rules/imports/resolvable.ts +23 -0
  371. package/src/rules/manifests-sorted.ts +23 -0
  372. package/src/rules/max-lines.ts +6 -6
  373. package/src/rules/suppressions/biome/configs.ts +12 -12
  374. package/src/rules/suppressions/biome/declarations.ts +7 -7
  375. package/src/rules/suppressions/biome/json.ts +3 -3
  376. package/src/rules/suppressions/biome/resolve.ts +3 -3
  377. package/src/rules/suppressions/biome/weakenings.ts +18 -19
  378. package/src/rules/suppressions/biome-overrides.ts +7 -7
  379. package/src/rules/suppressions/directives.ts +12 -12
  380. package/src/rules/suppressions/no-double-cast.ts +4 -4
  381. package/src/rules/suppressions/no-inline.ts +13 -37
  382. package/src/rules/suppressions/stylesheet-comments.ts +4 -4
  383. package/src/rules/syntax.ts +4 -4
  384. package/src/standard-schema.ts +1 -1
  385. package/src/vitest.ts +22 -0
  386. package/tsconfig/base.json +21 -21
  387. package/tsconfig/package.json +2 -2
@@ -4,13 +4,13 @@ import { SetupFailure } from "../failure.ts";
4
4
 
5
5
  export interface GitResult {
6
6
  readonly code: number;
7
- readonly stdout: string;
8
7
  readonly stderr: string;
8
+ readonly stdout: string;
9
9
  }
10
10
 
11
11
  export type Git = ChildProcessSpawner.ChildProcessSpawner;
12
12
 
13
- export const git = (cwd: string, args: ReadonlyArray<string>, env?: Readonly<Record<string, string>>): Effect.Effect<GitResult, SetupFailure, Git> =>
13
+ export const git = (cwd: string, args: readonly string[], env?: Readonly<Record<string, string>>): Effect.Effect<GitResult, SetupFailure, Git> =>
14
14
  Effect.scoped(
15
15
  Effect.gen(function* () {
16
16
  const spawner = yield* ChildProcessSpawner.ChildProcessSpawner;
@@ -23,7 +23,7 @@ export const git = (cwd: string, args: ReadonlyArray<string>, env?: Readonly<Rec
23
23
  }),
24
24
  ).pipe(Effect.mapError((error) => new SetupFailure({ message: `cannot run git: ${error.message}` })));
25
25
 
26
- export const gitOrFail = (cwd: string, args: ReadonlyArray<string>): Effect.Effect<string, SetupFailure, Git> =>
26
+ export const gitOrFail = (cwd: string, args: readonly string[]): Effect.Effect<string, SetupFailure, Git> =>
27
27
  Effect.flatMap(git(cwd, args), (result) =>
28
28
  result.code === 0
29
29
  ? Effect.succeed(result.stdout)
@@ -20,7 +20,7 @@ const parse = (output: string): Changes => {
20
20
  const moves = new Map<string, string>();
21
21
  for (let index = 0; index < fields.length - 1; ) {
22
22
  const status = fields[index] ?? "";
23
- const paths = fields.slice(index + 1, index + (/^[RC]/.test(status) ? 3 : 2));
23
+ const paths = fields.slice(index + 1, index + (/^[RC]/u.test(status) ? 3 : 2));
24
24
  for (const path of paths) {
25
25
  files.add(path);
26
26
  }
@@ -34,7 +34,7 @@ const parse = (output: string): Changes => {
34
34
  };
35
35
 
36
36
  // Paths are relative to the root, and only tracked files count: a move is seen once both of its sides are staged.
37
- export const changesSince = (root: string, revision: ReadonlyArray<string>): Effect.Effect<Changes, SetupFailure, Git> =>
37
+ export const changesSince = (root: string, revision: readonly string[]): Effect.Effect<Changes, SetupFailure, Git> =>
38
38
  Effect.map(
39
39
  gitOrFail(root, ["diff", "--no-color", "--no-ext-diff", "--find-renames", "--name-status", "-z", "--relative", ...revision, "--"]),
40
40
  parse,
@@ -0,0 +1,54 @@
1
+ import ts from "typescript";
2
+
3
+ export type Ambient = (specifier: string, projectFiles: ReadonlySet<string>) => boolean;
4
+
5
+ const WILDCARD = "*";
6
+
7
+ function isPattern(name: string): boolean {
8
+ return name !== WILDCARD && name.split(WILDCARD).length <= 2;
9
+ }
10
+
11
+ function declaredIn(source: ts.SourceFile): readonly string[] {
12
+ if (ts.isExternalModule(source)) {
13
+ return [];
14
+ }
15
+ return source.statements.flatMap((statement) =>
16
+ ts.isModuleDeclaration(statement) && ts.isStringLiteral(statement.name) && isPattern(statement.name.text) ? [statement.name.text] : [],
17
+ );
18
+ }
19
+
20
+ function wildcardMatches(pattern: string, value: string): boolean {
21
+ const [head = "", ...rest] = pattern.split(WILDCARD);
22
+ const tail = rest.pop();
23
+ if (tail === undefined) {
24
+ return pattern === value;
25
+ }
26
+ const end = value.length - tail.length;
27
+ if (end < head.length || !value.startsWith(head) || !value.endsWith(tail)) {
28
+ return false;
29
+ }
30
+ let at = head.length;
31
+ for (const middle of rest) {
32
+ const found = value.indexOf(middle, at);
33
+ if (found === -1 || found + middle.length > end) {
34
+ return false;
35
+ }
36
+ at = found + middle.length;
37
+ }
38
+ return true;
39
+ }
40
+
41
+ function isBare(specifier: string): boolean {
42
+ return !(specifier.startsWith(".") || specifier.startsWith("/"));
43
+ }
44
+
45
+ // A module declared in a script, not a module, is ambient: it exists for the files of the project that includes the script.
46
+ export function ambientModules(sources: readonly { readonly path: string; readonly syntax: ts.SourceFile }[]): Ambient {
47
+ const declarations = sources.flatMap(({ path, syntax }) => {
48
+ const patterns = declaredIn(syntax);
49
+ return patterns.length === 0 ? [] : [{ path, patterns }];
50
+ });
51
+ return (specifier, projectFiles) =>
52
+ isBare(specifier)
53
+ && declarations.some(({ path, patterns }) => projectFiles.has(path) && patterns.some((pattern) => wildcardMatches(pattern, specifier)));
54
+ }
@@ -0,0 +1,80 @@
1
+ interface Frame {
2
+ next: number;
3
+ readonly node: string;
4
+ }
5
+
6
+ interface Search {
7
+ readonly adjacency: ReadonlyMap<string, readonly string[]>;
8
+ readonly found: string[][];
9
+ readonly index: Map<string, number>;
10
+ readonly low: Map<string, number>;
11
+ readonly open: Set<string>;
12
+ readonly stack: string[];
13
+ }
14
+
15
+ function byPath(left: string, right: string): number {
16
+ return left.localeCompare(right);
17
+ }
18
+
19
+ function enter(search: Search, frames: Frame[], node: string): void {
20
+ const index = search.index.size;
21
+ search.index.set(node, index);
22
+ search.low.set(node, index);
23
+ search.stack.push(node);
24
+ search.open.add(node);
25
+ frames.push({ next: 0, node });
26
+ }
27
+
28
+ function lowOf(search: Search, node: string): number {
29
+ return search.low.get(node) ?? 0;
30
+ }
31
+
32
+ function close(search: Search, node: string): void {
33
+ if (lowOf(search, node) !== search.index.get(node)) {
34
+ return;
35
+ }
36
+ const component: string[] = [];
37
+ for (let member = search.stack.pop(); member !== undefined; member = member === node ? undefined : search.stack.pop()) {
38
+ search.open.delete(member);
39
+ component.push(member);
40
+ }
41
+ const loops = (search.adjacency.get(node) ?? []).includes(node);
42
+ if (component.length > 1 || loops) {
43
+ search.found.push(component.sort(byPath));
44
+ }
45
+ }
46
+
47
+ function step(search: Search, frames: Frame[], frame: Frame): void {
48
+ const neighbour = (search.adjacency.get(frame.node) ?? [])[frame.next];
49
+ if (neighbour === undefined) {
50
+ frames.pop();
51
+ const parent = frames.at(-1);
52
+ if (parent !== undefined) {
53
+ search.low.set(parent.node, Math.min(lowOf(search, parent.node), lowOf(search, frame.node)));
54
+ }
55
+ close(search, frame.node);
56
+ return;
57
+ }
58
+ frame.next += 1;
59
+ if (!search.index.has(neighbour)) {
60
+ enter(search, frames, neighbour);
61
+ } else if (search.open.has(neighbour)) {
62
+ search.low.set(frame.node, Math.min(lowOf(search, frame.node), search.index.get(neighbour) ?? 0));
63
+ }
64
+ }
65
+
66
+ // Tarjan's algorithm with an explicit stack, so a long import chain cannot overflow the call stack.
67
+ export function cyclicComponents(adjacency: ReadonlyMap<string, readonly string[]>): readonly (readonly string[])[] {
68
+ const search: Search = { adjacency, found: [], index: new Map(), low: new Map(), open: new Set(), stack: [] };
69
+ for (const node of [...adjacency.keys()].sort(byPath)) {
70
+ if (search.index.has(node)) {
71
+ continue;
72
+ }
73
+ const frames: Frame[] = [];
74
+ enter(search, frames, node);
75
+ for (let frame = frames.at(-1); frame !== undefined; frame = frames.at(-1)) {
76
+ step(search, frames, frame);
77
+ }
78
+ }
79
+ return search.found.sort((left, right) => byPath(left[0] ?? "", right[0] ?? ""));
80
+ }
@@ -0,0 +1,129 @@
1
+ import ts from "typescript";
2
+
3
+ export type ImportKind = "import" | "path-reference" | "resolve" | "type-reference";
4
+
5
+ export interface ImportRequest {
6
+ readonly kind: ImportKind;
7
+ readonly line: number;
8
+ readonly specifier: string;
9
+ readonly type: boolean;
10
+ }
11
+
12
+ interface Found {
13
+ readonly kind: ImportKind;
14
+ readonly node: ts.Node | undefined;
15
+ readonly type: boolean;
16
+ }
17
+
18
+ const MAY_CALL = /\b(?:import|require)\s*\(|\b(?:require|import\.meta)\.resolve\s*\(/u;
19
+
20
+ const JSDOC_IMPORT = "@import";
21
+
22
+ function lineAt(source: ts.SourceFile, position: number): number {
23
+ return source.getLineAndCharacterOfPosition(position).line + 1;
24
+ }
25
+
26
+ function declared(statement: ts.Statement): Found | undefined {
27
+ if (ts.isImportDeclaration(statement)) {
28
+ return { kind: "import", node: statement.moduleSpecifier, type: statement.importClause?.isTypeOnly === true };
29
+ }
30
+ if (ts.isExportDeclaration(statement)) {
31
+ return { kind: "import", node: statement.moduleSpecifier, type: statement.isTypeOnly };
32
+ }
33
+ if (ts.isImportEqualsDeclaration(statement) && ts.isExternalModuleReference(statement.moduleReference)) {
34
+ return { kind: "import", node: statement.moduleReference.expression, type: statement.isTypeOnly };
35
+ }
36
+ return undefined;
37
+ }
38
+
39
+ function isRequire(call: ts.CallExpression): boolean {
40
+ return ts.isIdentifier(call.expression) && call.expression.text === "require";
41
+ }
42
+
43
+ function isImportMeta(node: ts.Expression): boolean {
44
+ return ts.isMetaProperty(node) && node.keywordToken === ts.SyntaxKind.ImportKeyword && node.name.text === "meta";
45
+ }
46
+
47
+ function resolver(call: ts.CallExpression): "meta" | "require" | undefined {
48
+ const callee = call.expression;
49
+ if (!ts.isPropertyAccessExpression(callee) || callee.name.text !== "resolve") {
50
+ return undefined;
51
+ }
52
+ if (ts.isIdentifier(callee.expression) && callee.expression.text === "require") {
53
+ return "require";
54
+ }
55
+ return isImportMeta(callee.expression) ? "meta" : undefined;
56
+ }
57
+
58
+ // Node resolves a relative import.meta.resolve() by URL arithmetic alone, so the path need not exist.
59
+ function isRelative(node: ts.Node | undefined): boolean {
60
+ return node !== undefined && ts.isStringLiteralLike(node) && (node.text.startsWith(".") || node.text.startsWith("/"));
61
+ }
62
+
63
+ function called(call: ts.CallExpression): Found | undefined {
64
+ const [argument] = call.arguments;
65
+ if (call.expression.kind === ts.SyntaxKind.ImportKeyword || isRequire(call)) {
66
+ return { kind: "import", node: argument, type: false };
67
+ }
68
+ const how = resolver(call);
69
+ const checked = how === "require" || (how === "meta" && !isRelative(argument));
70
+ return checked ? { kind: "resolve", node: argument, type: false } : undefined;
71
+ }
72
+
73
+ function nested(node: ts.Node): Found | undefined {
74
+ if (ts.isCallExpression(node)) {
75
+ return called(node);
76
+ }
77
+ if (ts.isImportTypeNode(node) && ts.isLiteralTypeNode(node.argument)) {
78
+ return { kind: "import", node: node.argument.literal, type: true };
79
+ }
80
+ return undefined;
81
+ }
82
+
83
+ function requestOf(source: ts.SourceFile, at: ts.Node, found: Found | undefined): readonly ImportRequest[] {
84
+ if (found?.node === undefined || !ts.isStringLiteralLike(found.node)) {
85
+ return [];
86
+ }
87
+ return [{ kind: found.kind, line: lineAt(source, at.getStart(source)), specifier: found.node.text, type: found.type }];
88
+ }
89
+
90
+ function nestedRequests(source: ts.SourceFile): readonly ImportRequest[] {
91
+ if (!MAY_CALL.test(source.text)) {
92
+ return [];
93
+ }
94
+ const requests: ImportRequest[] = [];
95
+ function visit(node: ts.Node): void {
96
+ requests.push(...requestOf(source, node, nested(node)));
97
+ ts.forEachChild(node, visit);
98
+ }
99
+ visit(source);
100
+ return requests;
101
+ }
102
+
103
+ function jsDocRequests(source: ts.SourceFile): readonly ImportRequest[] {
104
+ if (!source.text.includes(JSDOC_IMPORT)) {
105
+ return [];
106
+ }
107
+ const tags = [...source.statements, source.endOfFileToken]
108
+ .flatMap((node) => ts.getJSDocCommentsAndTags(node))
109
+ .flatMap((doc) => (ts.isJSDoc(doc) ? (doc.tags ?? []) : [doc]))
110
+ .filter(ts.isJSDocImportTag);
111
+ return tags.flatMap((tag) => requestOf(source, tag, { kind: "import", node: tag.moduleSpecifier, type: true }));
112
+ }
113
+
114
+ function referenceRequests(source: ts.SourceFile): readonly ImportRequest[] {
115
+ function requestFor(kind: ImportKind): (reference: ts.FileReference) => ImportRequest {
116
+ return (reference) => ({ kind, line: lineAt(source, reference.pos), specifier: reference.fileName, type: true });
117
+ }
118
+ return [...source.typeReferenceDirectives.map(requestFor("type-reference")), ...source.referencedFiles.map(requestFor("path-reference"))];
119
+ }
120
+
121
+ export function importsOf(source: ts.SourceFile, declarationFile: boolean): readonly ImportRequest[] {
122
+ const all = [
123
+ ...referenceRequests(source),
124
+ ...source.statements.flatMap((statement) => requestOf(source, statement, declared(statement))),
125
+ ...jsDocRequests(source),
126
+ ...nestedRequests(source),
127
+ ];
128
+ return declarationFile ? all.map((request) => ({ ...request, type: true })) : all;
129
+ }
@@ -0,0 +1,33 @@
1
+ import type { Examples, Fence, Prohibition, Selector, Target } from "./model.ts";
2
+
3
+ interface Draft {
4
+ readonly from: Selector;
5
+ readonly name: string;
6
+ readonly rationale: string;
7
+ }
8
+
9
+ interface Demonstrable {
10
+ readonly demonstratedBy: (examples: Examples) => Fence;
11
+ }
12
+
13
+ interface Prohibitions {
14
+ readonly mayImportOnly: (...subjects: readonly string[]) => { readonly of: (unit: Target) => Demonstrable };
15
+ readonly mayNotImport: (target: Target) => Demonstrable;
16
+ readonly mayNotReach: (target: Target) => Demonstrable;
17
+ }
18
+
19
+ function demonstrable(draft: Draft, prohibition: Prohibition): Demonstrable {
20
+ return { demonstratedBy: (examples) => ({ _tag: "Fence", examples, from: draft.from, name: draft.name, prohibition, rationale: draft.rationale }) };
21
+ }
22
+
23
+ function prohibitions(draft: Draft): Prohibitions {
24
+ return {
25
+ mayImportOnly: (...subjects) => ({ of: (unit) => demonstrable(draft, { kind: "vocabulary", subjects, unit: unit.selector }) }),
26
+ mayNotImport: (target) => demonstrable(draft, { kind: "import", to: target.selector }),
27
+ mayNotReach: (target) => demonstrable(draft, { kind: "reach", to: target.selector }),
28
+ };
29
+ }
30
+
31
+ export function fence(name: string): { readonly because: (rationale: string) => { readonly from: (subject: Target) => Prohibitions } } {
32
+ return { because: (rationale) => ({ from: (subject) => prohibitions({ from: subject.selector, name, rationale }) }) };
33
+ }
@@ -0,0 +1,75 @@
1
+ import type { Finding } from "../../rule.ts";
2
+ import type { ImportEdge, ImportGraph } from "../graph.ts";
3
+ import { type Compiler, compile, type Matcher } from "./match.ts";
4
+ import type { Fence } from "./model.ts";
5
+ import { labelOf, outgoing, reachedFrom } from "./reach.ts";
6
+ import { compileVocabulary, type Vocabulary } from "./vocabulary.ts";
7
+
8
+ export type FenceGraph = Pick<ImportGraph, "edges" | "modules">;
9
+
10
+ export type Evaluate = (graph: FenceGraph) => readonly Finding[];
11
+
12
+ export interface CompiledFence {
13
+ readonly evaluate: Evaluate;
14
+ readonly fence: Fence;
15
+ }
16
+
17
+ function across(fence: Fence): string {
18
+ return `across the fence "${fence.name}": ${fence.rationale}`;
19
+ }
20
+
21
+ function vocabularyFinding(fence: Fence, vocabulary: Vocabulary, subjects: readonly string[], edge: ImportEdge): readonly Finding[] {
22
+ const subject = vocabulary.subjectOf(edge.to);
23
+ if (subject === undefined || subjects.includes(subject)) {
24
+ return [];
25
+ }
26
+ const message = `Imports "${subject}" of ${vocabulary.folder} ${across(fence)} Use only ${subjects.join(", ")}.`;
27
+ return [{ file: edge.from, line: edge.line, message, subject: fence.name }];
28
+ }
29
+
30
+ function vocabularyFindings(compiler: Compiler, fence: Fence, importer: (path: string) => boolean, where: string): Evaluate {
31
+ if (fence.prohibition.kind !== "vocabulary") {
32
+ return () => [];
33
+ }
34
+ const subjects = fence.prohibition.subjects;
35
+ const vocabulary = compileVocabulary(compiler, fence.prohibition.unit, subjects, `${where}.of`);
36
+ if (vocabulary === undefined) {
37
+ return () => [];
38
+ }
39
+ return (graph) => graph.edges.flatMap((edge) => (importer(edge.from) ? vocabularyFinding(fence, vocabulary, subjects, edge) : []));
40
+ }
41
+
42
+ function importFindings(fence: Fence, importer: (path: string) => boolean, to: Matcher): Evaluate {
43
+ return (graph) =>
44
+ graph.edges
45
+ .filter((edge) => importer(edge.from) && to(edge.to))
46
+ .map((edge) => ({ file: edge.from, line: edge.line, message: `Imports ${labelOf(edge.to)} ${across(fence)}`, subject: fence.name }));
47
+ }
48
+
49
+ function reachFindings(fence: Fence, importer: (path: string) => boolean, to: Matcher): Evaluate {
50
+ return (graph) => {
51
+ const edgesFrom = outgoing(graph.edges);
52
+ return graph.modules.filter(importer).flatMap((module) =>
53
+ reachedFrom(module, edgesFrom, to).map((reached) => ({
54
+ file: module,
55
+ line: reached.line,
56
+ message: `Reaches ${labelOf(reached.target)} through ${reached.path.join(" -> ")} ${across(fence)}`,
57
+ subject: fence.name,
58
+ })),
59
+ );
60
+ };
61
+ }
62
+
63
+ export function compileFence(compiler: Compiler, fence: Fence): CompiledFence {
64
+ const where = `fence "${fence.name}"`;
65
+ const from = compile(compiler, fence.from, `${where}.from`);
66
+ function importer(path: string): boolean {
67
+ return from({ kind: "file", path });
68
+ }
69
+ const prohibition = fence.prohibition;
70
+ if (prohibition.kind === "vocabulary") {
71
+ return { evaluate: vocabularyFindings(compiler, fence, importer, where), fence };
72
+ }
73
+ const to = compile(compiler, prohibition.to, `${where}.to`);
74
+ return { evaluate: prohibition.kind === "import" ? importFindings(fence, importer, to) : reachFindings(fence, importer, to), fence };
75
+ }
@@ -0,0 +1,148 @@
1
+ import { builtinName, type Endpoint, packageNameOf } from "../resolve.ts";
2
+ import { packageOf, type WorkspacePackage } from "../workspace.ts";
3
+ import type { Selector } from "./model.ts";
4
+
5
+ export interface PolicyScope {
6
+ readonly files: readonly string[];
7
+ readonly packages: readonly WorkspacePackage[];
8
+ }
9
+
10
+ export type Matcher = (endpoint: Endpoint) => boolean;
11
+
12
+ export interface Compiler {
13
+ readonly issues: string[];
14
+ readonly scope: PolicyScope;
15
+ }
16
+
17
+ const TRAILING_SLASHES = /\/+$/u;
18
+
19
+ function unscoped(name: string): string {
20
+ return name.startsWith("@") ? name.slice(name.indexOf("/") + 1) : name;
21
+ }
22
+
23
+ export function trimmed(path: string): string {
24
+ return path.replace(TRAILING_SLASHES, "");
25
+ }
26
+
27
+ export function holdsFiles(compiler: Compiler, folder: string): boolean {
28
+ return compiler.scope.files.some((file) => file.startsWith(`${folder}/`));
29
+ }
30
+
31
+ export function packageNamed(compiler: Compiler, name: string, where: string): WorkspacePackage | undefined {
32
+ const found = compiler.scope.packages.filter((candidate) => candidate.name === name || unscoped(candidate.name) === name);
33
+ if (found.length !== 1) {
34
+ compiler.issues.push(`${where}: ${found.length === 0 ? "no workspace package is named" : "more than one workspace package is named"} "${name}"`);
35
+ }
36
+ return found.length === 1 ? found[0] : undefined;
37
+ }
38
+
39
+ function checkNames(
40
+ compiler: Compiler,
41
+ where: string,
42
+ kind: string,
43
+ values: readonly string[],
44
+ problemOf: (value: string) => string | undefined,
45
+ ): void {
46
+ if (values.length === 0) {
47
+ compiler.issues.push(`${where}: ${kind}() names nothing`);
48
+ }
49
+ for (const value of values) {
50
+ const problem = problemOf(value);
51
+ if (problem !== undefined) {
52
+ compiler.issues.push(`${where}: ${kind}("${value}") ${problem}`);
53
+ }
54
+ }
55
+ }
56
+
57
+ function fileWhere(accepts: (path: string) => boolean): Matcher {
58
+ return (endpoint) => endpoint.kind === "file" && accepts(endpoint.path);
59
+ }
60
+
61
+ function externalWhere(accepts: (name: string) => boolean): Matcher {
62
+ return (endpoint) => endpoint.kind === "external" && accepts(endpoint.package);
63
+ }
64
+
65
+ function isPackageName(name: string): boolean {
66
+ const scoped = !name.startsWith("@") || name.includes("/");
67
+ return name.trim() !== "" && !name.startsWith(".") && !name.startsWith("/") && scoped && packageNameOf(name) === name;
68
+ }
69
+
70
+ function underAny(prefixes: readonly string[], value: string): boolean {
71
+ return prefixes.some((prefix) => value === prefix || value.startsWith(`${prefix}/`));
72
+ }
73
+
74
+ function compilePackages(compiler: Compiler, names: readonly string[], where: string): Matcher {
75
+ checkNames(compiler, where, "packages", names, () => undefined);
76
+ const directories = new Set(names.flatMap((name) => packageNamed(compiler, name, where)?.directory ?? []));
77
+ return fileWhere((path) => directories.has(packageOf(compiler.scope.packages, path)?.directory ?? ""));
78
+ }
79
+
80
+ function compileFolders(compiler: Compiler, paths: readonly string[], where: string): Matcher {
81
+ const folders = paths.map(trimmed);
82
+ checkNames(compiler, where, "folders", folders, (folder) => (holdsFiles(compiler, folder) ? undefined : "holds no checked file"));
83
+ return fileWhere((path) => folders.some((folder) => path.startsWith(`${folder}/`)));
84
+ }
85
+
86
+ function compileFiles(compiler: Compiler, paths: readonly string[], where: string): Matcher {
87
+ checkNames(compiler, where, "files", paths, (path) => (compiler.scope.files.includes(path) ? undefined : "is no checked file"));
88
+ return fileWhere((path) => paths.includes(path));
89
+ }
90
+
91
+ function compileModules(compiler: Compiler, names: readonly string[], where: string): Matcher {
92
+ checkNames(compiler, where, "modules", names, (name) =>
93
+ builtinName(name) !== undefined || isPackageName(name) ? undefined : "is no package name",
94
+ );
95
+ const normalized = names.map((name) => builtinName(name) ?? name);
96
+ return externalWhere((name) => underAny(normalized, name));
97
+ }
98
+
99
+ function compileScopes(compiler: Compiler, names: readonly string[], where: string): Matcher {
100
+ checkNames(compiler, where, "scopes", names, (name) =>
101
+ name.startsWith("@") && name.length > 1 && !name.includes("/") ? undefined : 'is no scope such as "@types"',
102
+ );
103
+ return externalWhere((name) => names.some((scope) => name.startsWith(`${scope}/`)));
104
+ }
105
+
106
+ function compileWorkspace(compiler: Compiler, where: string): Matcher {
107
+ if (compiler.scope.packages.length === 0) {
108
+ compiler.issues.push(
109
+ `${where}: workspace selects nothing, since no named package.json among the sources is a member of the workspace that pnpm-workspace.yaml or the root package.json declares`,
110
+ );
111
+ }
112
+ return fileWhere((path) => packageOf(compiler.scope.packages, path) !== undefined);
113
+ }
114
+
115
+ function compileGroup(compiler: Compiler, members: readonly Selector[], where: string, kind: string): readonly Matcher[] {
116
+ if (members.length === 0) {
117
+ compiler.issues.push(`${where}: ${kind}() names nothing`);
118
+ }
119
+ return members.map((member, index) => compile(compiler, member, `${where}.${kind}[${index}]`));
120
+ }
121
+
122
+ export function compile(compiler: Compiler, selector: Selector, where: string): Matcher {
123
+ switch (selector.kind) {
124
+ case "anything":
125
+ return () => true;
126
+ case "anyOf": {
127
+ const members = compileGroup(compiler, selector.members, where, "anyOf");
128
+ return (endpoint) => members.some((member) => member(endpoint));
129
+ }
130
+ case "except": {
131
+ const base = compile(compiler, selector.base, where);
132
+ const excluded = compileGroup(compiler, selector.excluded, where, "except");
133
+ return (endpoint) => base(endpoint) && !excluded.some((member) => member(endpoint));
134
+ }
135
+ case "files":
136
+ return compileFiles(compiler, selector.paths, where);
137
+ case "folders":
138
+ return compileFolders(compiler, selector.paths, where);
139
+ case "modules":
140
+ return compileModules(compiler, selector.names, where);
141
+ case "packages":
142
+ return compilePackages(compiler, selector.names, where);
143
+ case "scopes":
144
+ return compileScopes(compiler, selector.names, where);
145
+ case "workspace":
146
+ return compileWorkspace(compiler, where);
147
+ }
148
+ }
@@ -0,0 +1,42 @@
1
+ export type Selector =
2
+ | { readonly kind: "anything" }
3
+ | { readonly kind: "anyOf"; readonly members: readonly Selector[] }
4
+ | { readonly kind: "except"; readonly base: Selector; readonly excluded: readonly Selector[] }
5
+ | { readonly kind: "files"; readonly paths: readonly string[] }
6
+ | { readonly kind: "folders"; readonly paths: readonly string[] }
7
+ | { readonly kind: "modules"; readonly names: readonly string[] }
8
+ | { readonly kind: "packages"; readonly names: readonly string[] }
9
+ | { readonly kind: "scopes"; readonly names: readonly string[] }
10
+ | { readonly kind: "workspace" };
11
+
12
+ export interface Target {
13
+ readonly except: (...excluded: readonly Target[]) => Target;
14
+ readonly selector: Selector;
15
+ }
16
+
17
+ export interface External {
18
+ readonly external: string;
19
+ }
20
+
21
+ export type ExampleStep = string | External;
22
+
23
+ export type Chain = readonly [string, ExampleStep, ...ExampleStep[]];
24
+
25
+ export interface Examples {
26
+ readonly illegal: Chain;
27
+ readonly legal: Chain;
28
+ }
29
+
30
+ export type Prohibition =
31
+ | { readonly kind: "import"; readonly to: Selector }
32
+ | { readonly kind: "reach"; readonly to: Selector }
33
+ | { readonly kind: "vocabulary"; readonly subjects: readonly string[]; readonly unit: Selector };
34
+
35
+ export interface Fence {
36
+ readonly _tag: "Fence";
37
+ readonly examples: Examples;
38
+ readonly from: Selector;
39
+ readonly name: string;
40
+ readonly prohibition: Prohibition;
41
+ readonly rationale: string;
42
+ }
@@ -0,0 +1,70 @@
1
+ import type { ImportEdge } from "../graph.ts";
2
+ import { type Endpoint, packageNameOf } from "../resolve.ts";
3
+ import { type CompiledFence, compileFence, type FenceGraph } from "./evaluate.ts";
4
+ import type { PolicyScope } from "./match.ts";
5
+ import type { Chain, ExampleStep, Fence } from "./model.ts";
6
+
7
+ function endpointOf(step: ExampleStep): Endpoint {
8
+ return typeof step === "string"
9
+ ? { kind: "file", path: step }
10
+ : { kind: "external", package: packageNameOf(step.external), specifier: step.external };
11
+ }
12
+
13
+ function chainLabel(chain: Chain): string {
14
+ return chain.map((step) => (typeof step === "string" ? step : step.external)).join(" -> ");
15
+ }
16
+
17
+ function graphOf(chain: Chain): FenceGraph | undefined {
18
+ const importers = chain.slice(0, -1);
19
+ const files = importers.flatMap((step) => (typeof step === "string" ? [step] : []));
20
+ if (files.length !== importers.length) {
21
+ return undefined;
22
+ }
23
+ const edges = files.map(
24
+ (from, index): ImportEdge => ({ from, kind: "import", line: 1, specifier: "", to: endpointOf(chain[index + 1] ?? from), type: false }),
25
+ );
26
+ return { edges, modules: files };
27
+ }
28
+
29
+ function crossedBy(policy: readonly CompiledFence[], graph: FenceGraph): readonly string[] {
30
+ return policy.filter((compiled) => compiled.evaluate(graph).length > 0).map((compiled) => compiled.fence.name);
31
+ }
32
+
33
+ function exampleIssues(policy: readonly CompiledFence[], fence: Fence): readonly string[] {
34
+ const where = `fence "${fence.name}"`;
35
+ const illegal = graphOf(fence.examples.illegal);
36
+ const legal = graphOf(fence.examples.legal);
37
+ if (illegal === undefined || legal === undefined) {
38
+ return [`${where}: an example imports from an external module; only its last step may be external()`];
39
+ }
40
+ const crossed = crossedBy(policy, illegal);
41
+ const passed = crossedBy(policy, legal);
42
+ return [
43
+ ...(crossed.length === 1 && crossed[0] === fence.name
44
+ ? []
45
+ : [
46
+ `${where}: the illegal example ${chainLabel(fence.examples.illegal)} must cross this fence alone, and crosses ${crossed.join(", ") || "none"}`,
47
+ ]),
48
+ ...(passed.length === 0 ? [] : [`${where}: the legal example ${chainLabel(fence.examples.legal)} crosses ${passed.join(", ")}`]),
49
+ ];
50
+ }
51
+
52
+ function declarationIssues(fences: readonly Fence[]): readonly string[] {
53
+ return fences.flatMap((fence, index) => [
54
+ ...(fence.name.trim() === "" ? [`fence ${index}: has no name`] : []),
55
+ ...(fences.findIndex((other) => other.name === fence.name) === index ? [] : [`fence "${fence.name}": an earlier fence has this name`]),
56
+ ...(fence.rationale.trim() === "" ? [`fence "${fence.name}": because() gives no reason`] : []),
57
+ ]);
58
+ }
59
+
60
+ export function compilePolicy(fences: readonly Fence[], scope: PolicyScope): readonly CompiledFence[] {
61
+ const issues = [...declarationIssues(fences)];
62
+ const policy = fences.map((fence) => compileFence({ issues, scope }, fence));
63
+ if (issues.length === 0) {
64
+ issues.push(...fences.flatMap((fence) => exampleIssues(policy, fence)));
65
+ }
66
+ if (issues.length > 0) {
67
+ throw new Error(`the fence policy is invalid:\n${issues.map((issue) => ` - ${issue}`).join("\n")}`);
68
+ }
69
+ return policy;
70
+ }