@ontrails/warden 1.0.0-beta.13 → 1.0.0-beta.15

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 (474) hide show
  1. package/.turbo/turbo-lint.log +1 -1
  2. package/CHANGELOG.md +30 -0
  3. package/README.md +31 -20
  4. package/dist/cli.d.ts +19 -2
  5. package/dist/cli.d.ts.map +1 -1
  6. package/dist/cli.js +261 -64
  7. package/dist/cli.js.map +1 -1
  8. package/dist/draft.d.ts +5 -0
  9. package/dist/draft.d.ts.map +1 -0
  10. package/dist/draft.js +16 -0
  11. package/dist/draft.js.map +1 -0
  12. package/dist/drift.d.ts +10 -7
  13. package/dist/drift.d.ts.map +1 -1
  14. package/dist/drift.js +50 -16
  15. package/dist/drift.js.map +1 -1
  16. package/dist/formatters.d.ts +2 -1
  17. package/dist/formatters.d.ts.map +1 -1
  18. package/dist/formatters.js +15 -4
  19. package/dist/formatters.js.map +1 -1
  20. package/dist/index.d.ts +9 -17
  21. package/dist/index.d.ts.map +1 -1
  22. package/dist/index.js +10 -17
  23. package/dist/index.js.map +1 -1
  24. package/dist/rules/ast.d.ts +412 -7
  25. package/dist/rules/ast.d.ts.map +1 -1
  26. package/dist/rules/ast.js +1847 -102
  27. package/dist/rules/ast.js.map +1 -1
  28. package/dist/rules/circular-refs.d.ts +6 -0
  29. package/dist/rules/circular-refs.d.ts.map +1 -0
  30. package/dist/rules/circular-refs.js +83 -0
  31. package/dist/rules/circular-refs.js.map +1 -0
  32. package/dist/rules/context-no-surface-types.d.ts.map +1 -1
  33. package/dist/rules/context-no-surface-types.js +59 -3
  34. package/dist/rules/context-no-surface-types.js.map +1 -1
  35. package/dist/rules/contour-exists.d.ts +7 -0
  36. package/dist/rules/contour-exists.d.ts.map +1 -0
  37. package/dist/rules/contour-exists.js +113 -0
  38. package/dist/rules/contour-exists.js.map +1 -0
  39. package/dist/rules/contour-ids.d.ts +10 -0
  40. package/dist/rules/contour-ids.d.ts.map +1 -0
  41. package/dist/rules/contour-ids.js +12 -0
  42. package/dist/rules/contour-ids.js.map +1 -0
  43. package/dist/rules/cross-declarations.d.ts.map +1 -1
  44. package/dist/rules/cross-declarations.js +171 -57
  45. package/dist/rules/cross-declarations.js.map +1 -1
  46. package/dist/rules/dead-internal-trail.d.ts +3 -0
  47. package/dist/rules/dead-internal-trail.d.ts.map +1 -0
  48. package/dist/rules/dead-internal-trail.js +80 -0
  49. package/dist/rules/dead-internal-trail.js.map +1 -0
  50. package/dist/rules/draft-file-marking.d.ts +6 -0
  51. package/dist/rules/draft-file-marking.d.ts.map +1 -0
  52. package/dist/rules/draft-file-marking.js +87 -0
  53. package/dist/rules/draft-file-marking.js.map +1 -0
  54. package/dist/rules/draft-visible-debt.d.ts +12 -0
  55. package/dist/rules/draft-visible-debt.d.ts.map +1 -0
  56. package/dist/rules/draft-visible-debt.js +50 -0
  57. package/dist/rules/draft-visible-debt.js.map +1 -0
  58. package/dist/rules/error-mapping-completeness.d.ts +13 -0
  59. package/dist/rules/error-mapping-completeness.d.ts.map +1 -0
  60. package/dist/rules/error-mapping-completeness.js +160 -0
  61. package/dist/rules/error-mapping-completeness.js.map +1 -0
  62. package/dist/rules/example-valid.d.ts +6 -0
  63. package/dist/rules/example-valid.d.ts.map +1 -0
  64. package/dist/rules/example-valid.js +203 -0
  65. package/dist/rules/example-valid.js.map +1 -0
  66. package/dist/rules/fires-declarations.d.ts +16 -0
  67. package/dist/rules/fires-declarations.d.ts.map +1 -0
  68. package/dist/rules/fires-declarations.js +444 -0
  69. package/dist/rules/fires-declarations.js.map +1 -0
  70. package/dist/rules/implementation-returns-result.d.ts +9 -0
  71. package/dist/rules/implementation-returns-result.d.ts.map +1 -1
  72. package/dist/rules/implementation-returns-result.js +638 -76
  73. package/dist/rules/implementation-returns-result.js.map +1 -1
  74. package/dist/rules/incomplete-accessor-for-standard-op.d.ts +30 -0
  75. package/dist/rules/incomplete-accessor-for-standard-op.d.ts.map +1 -0
  76. package/dist/rules/incomplete-accessor-for-standard-op.js +226 -0
  77. package/dist/rules/incomplete-accessor-for-standard-op.js.map +1 -0
  78. package/dist/rules/incomplete-crud.d.ts +21 -0
  79. package/dist/rules/incomplete-crud.d.ts.map +1 -0
  80. package/dist/rules/incomplete-crud.js +368 -0
  81. package/dist/rules/incomplete-crud.js.map +1 -0
  82. package/dist/rules/index.d.ts +40 -7
  83. package/dist/rules/index.d.ts.map +1 -1
  84. package/dist/rules/index.js +91 -15
  85. package/dist/rules/index.js.map +1 -1
  86. package/dist/rules/intent-propagation.d.ts +3 -0
  87. package/dist/rules/intent-propagation.d.ts.map +1 -0
  88. package/dist/rules/intent-propagation.js +57 -0
  89. package/dist/rules/intent-propagation.js.map +1 -0
  90. package/dist/rules/missing-reconcile.d.ts +3 -0
  91. package/dist/rules/missing-reconcile.d.ts.map +1 -0
  92. package/dist/rules/missing-reconcile.js +44 -0
  93. package/dist/rules/missing-reconcile.js.map +1 -0
  94. package/dist/rules/missing-visibility.d.ts +3 -0
  95. package/dist/rules/missing-visibility.d.ts.map +1 -0
  96. package/dist/rules/missing-visibility.js +63 -0
  97. package/dist/rules/missing-visibility.js.map +1 -0
  98. package/dist/rules/no-direct-impl-in-route.d.ts.map +1 -1
  99. package/dist/rules/no-direct-impl-in-route.js +0 -3
  100. package/dist/rules/no-direct-impl-in-route.js.map +1 -1
  101. package/dist/rules/no-direct-implementation-call.js +1 -1
  102. package/dist/rules/no-direct-implementation-call.js.map +1 -1
  103. package/dist/rules/no-sync-result-assumption.d.ts.map +1 -1
  104. package/dist/rules/no-sync-result-assumption.js +870 -61
  105. package/dist/rules/no-sync-result-assumption.js.map +1 -1
  106. package/dist/rules/no-throw-in-detour-recover.d.ts +3 -0
  107. package/dist/rules/no-throw-in-detour-recover.d.ts.map +1 -0
  108. package/dist/rules/no-throw-in-detour-recover.js +147 -0
  109. package/dist/rules/no-throw-in-detour-recover.js.map +1 -0
  110. package/dist/rules/no-throw-in-detour-target.d.ts +4 -1
  111. package/dist/rules/no-throw-in-detour-target.d.ts.map +1 -1
  112. package/dist/rules/no-throw-in-detour-target.js +6 -3
  113. package/dist/rules/no-throw-in-detour-target.js.map +1 -1
  114. package/dist/rules/no-throw-in-implementation.d.ts +4 -2
  115. package/dist/rules/no-throw-in-implementation.d.ts.map +1 -1
  116. package/dist/rules/no-throw-in-implementation.js +6 -4
  117. package/dist/rules/no-throw-in-implementation.js.map +1 -1
  118. package/dist/rules/on-references-exist.d.ts +14 -0
  119. package/dist/rules/on-references-exist.d.ts.map +1 -0
  120. package/dist/rules/on-references-exist.js +109 -0
  121. package/dist/rules/on-references-exist.js.map +1 -0
  122. package/dist/rules/orphaned-signal.d.ts +3 -0
  123. package/dist/rules/orphaned-signal.d.ts.map +1 -0
  124. package/dist/rules/orphaned-signal.js +67 -0
  125. package/dist/rules/orphaned-signal.js.map +1 -0
  126. package/dist/rules/permit-governance.d.ts +3 -0
  127. package/dist/rules/permit-governance.d.ts.map +1 -0
  128. package/dist/rules/permit-governance.js +15 -0
  129. package/dist/rules/permit-governance.js.map +1 -0
  130. package/dist/rules/reference-exists.d.ts +6 -0
  131. package/dist/rules/reference-exists.d.ts.map +1 -0
  132. package/dist/rules/reference-exists.js +47 -0
  133. package/dist/rules/reference-exists.js.map +1 -0
  134. package/dist/rules/registry-names.d.ts +8 -0
  135. package/dist/rules/registry-names.d.ts.map +1 -0
  136. package/dist/rules/registry-names.js +83 -0
  137. package/dist/rules/registry-names.js.map +1 -0
  138. package/dist/rules/resource-declarations.d.ts +14 -0
  139. package/dist/rules/resource-declarations.d.ts.map +1 -0
  140. package/dist/rules/resource-declarations.js +413 -0
  141. package/dist/rules/resource-declarations.js.map +1 -0
  142. package/dist/rules/resource-exists.d.ts +6 -0
  143. package/dist/rules/resource-exists.d.ts.map +1 -0
  144. package/dist/rules/resource-exists.js +90 -0
  145. package/dist/rules/resource-exists.js.map +1 -0
  146. package/dist/rules/resource-id-grammar.d.ts +3 -0
  147. package/dist/rules/resource-id-grammar.d.ts.map +1 -0
  148. package/dist/rules/resource-id-grammar.js +39 -0
  149. package/dist/rules/resource-id-grammar.js.map +1 -0
  150. package/dist/rules/specs.d.ts.map +1 -1
  151. package/dist/rules/specs.js +5 -1
  152. package/dist/rules/specs.js.map +1 -1
  153. package/dist/rules/types.d.ts +53 -4
  154. package/dist/rules/types.d.ts.map +1 -1
  155. package/dist/rules/unreachable-detour-shadowing.d.ts +3 -0
  156. package/dist/rules/unreachable-detour-shadowing.d.ts.map +1 -0
  157. package/dist/rules/unreachable-detour-shadowing.js +202 -0
  158. package/dist/rules/unreachable-detour-shadowing.js.map +1 -0
  159. package/dist/rules/valid-describe-refs.d.ts.map +1 -1
  160. package/dist/rules/valid-describe-refs.js +132 -16
  161. package/dist/rules/valid-describe-refs.js.map +1 -1
  162. package/dist/rules/valid-detour-contract.d.ts +3 -0
  163. package/dist/rules/valid-detour-contract.d.ts.map +1 -0
  164. package/dist/rules/valid-detour-contract.js +47 -0
  165. package/dist/rules/valid-detour-contract.js.map +1 -0
  166. package/dist/rules/valid-detour-refs.d.ts.map +1 -1
  167. package/dist/rules/valid-detour-refs.js +73 -82
  168. package/dist/rules/valid-detour-refs.js.map +1 -1
  169. package/dist/rules/warden-export-symmetry.d.ts +7 -0
  170. package/dist/rules/warden-export-symmetry.d.ts.map +1 -0
  171. package/dist/rules/warden-export-symmetry.js +352 -0
  172. package/dist/rules/warden-export-symmetry.js.map +1 -0
  173. package/dist/rules/warden-rules-use-ast.d.ts +17 -0
  174. package/dist/rules/warden-rules-use-ast.d.ts.map +1 -0
  175. package/dist/rules/warden-rules-use-ast.js +778 -0
  176. package/dist/rules/warden-rules-use-ast.js.map +1 -0
  177. package/dist/trails/circular-refs.trail.d.ts +24 -0
  178. package/dist/trails/circular-refs.trail.d.ts.map +1 -0
  179. package/dist/trails/circular-refs.trail.js +29 -0
  180. package/dist/trails/circular-refs.trail.js.map +1 -0
  181. package/dist/trails/context-no-surface-types.trail.d.ts +2 -2
  182. package/dist/trails/context-no-surface-types.trail.d.ts.map +1 -1
  183. package/dist/trails/context-no-trailhead-types.trail.d.ts +2 -2
  184. package/dist/trails/context-no-trailhead-types.trail.d.ts.map +1 -1
  185. package/dist/trails/contour-exists.trail.d.ts +24 -0
  186. package/dist/trails/contour-exists.trail.d.ts.map +1 -0
  187. package/dist/trails/contour-exists.trail.js +21 -0
  188. package/dist/trails/contour-exists.trail.js.map +1 -0
  189. package/dist/trails/cross-declarations.trail.d.ts +2 -2
  190. package/dist/trails/cross-declarations.trail.d.ts.map +1 -1
  191. package/dist/trails/dead-internal-trail.trail.d.ts +24 -0
  192. package/dist/trails/dead-internal-trail.trail.d.ts.map +1 -0
  193. package/dist/trails/dead-internal-trail.trail.js +26 -0
  194. package/dist/trails/dead-internal-trail.trail.js.map +1 -0
  195. package/dist/trails/{provision-declarations.trail.d.ts → draft-file-marking.trail.d.ts} +3 -3
  196. package/dist/trails/draft-file-marking.trail.d.ts.map +1 -0
  197. package/dist/trails/draft-file-marking.trail.js +16 -0
  198. package/dist/trails/draft-file-marking.trail.js.map +1 -0
  199. package/dist/trails/draft-visible-debt.trail.d.ts +13 -0
  200. package/dist/trails/draft-visible-debt.trail.d.ts.map +1 -0
  201. package/dist/trails/draft-visible-debt.trail.js +16 -0
  202. package/dist/trails/draft-visible-debt.trail.js.map +1 -0
  203. package/dist/trails/error-mapping-completeness.trail.d.ts +13 -0
  204. package/dist/trails/error-mapping-completeness.trail.d.ts.map +1 -0
  205. package/dist/trails/error-mapping-completeness.trail.js +29 -0
  206. package/dist/trails/error-mapping-completeness.trail.js.map +1 -0
  207. package/dist/trails/{follow-declarations.trail.d.ts → example-valid.trail.d.ts} +3 -3
  208. package/dist/trails/example-valid.trail.d.ts.map +1 -0
  209. package/dist/trails/example-valid.trail.js +25 -0
  210. package/dist/trails/example-valid.trail.js.map +1 -0
  211. package/dist/trails/fires-declarations.trail.d.ts +13 -0
  212. package/dist/trails/fires-declarations.trail.d.ts.map +1 -0
  213. package/dist/trails/fires-declarations.trail.js +22 -0
  214. package/dist/trails/fires-declarations.trail.js.map +1 -0
  215. package/dist/trails/implementation-returns-result.trail.d.ts +2 -2
  216. package/dist/trails/implementation-returns-result.trail.d.ts.map +1 -1
  217. package/dist/trails/incomplete-accessor-for-standard-op.trail.d.ts +12 -0
  218. package/dist/trails/incomplete-accessor-for-standard-op.trail.d.ts.map +1 -0
  219. package/dist/trails/incomplete-accessor-for-standard-op.trail.js +60 -0
  220. package/dist/trails/incomplete-accessor-for-standard-op.trail.js.map +1 -0
  221. package/dist/trails/incomplete-crud.trail.d.ts +24 -0
  222. package/dist/trails/incomplete-crud.trail.d.ts.map +1 -0
  223. package/dist/trails/incomplete-crud.trail.js +39 -0
  224. package/dist/trails/incomplete-crud.trail.js.map +1 -0
  225. package/dist/trails/index.d.ts +29 -7
  226. package/dist/trails/index.d.ts.map +1 -1
  227. package/dist/trails/index.js +28 -6
  228. package/dist/trails/index.js.map +1 -1
  229. package/dist/trails/intent-propagation.trail.d.ts +24 -0
  230. package/dist/trails/intent-propagation.trail.d.ts.map +1 -0
  231. package/dist/trails/intent-propagation.trail.js +30 -0
  232. package/dist/trails/intent-propagation.trail.js.map +1 -0
  233. package/dist/trails/missing-reconcile.trail.d.ts +24 -0
  234. package/dist/trails/missing-reconcile.trail.d.ts.map +1 -0
  235. package/dist/trails/missing-reconcile.trail.js +33 -0
  236. package/dist/trails/missing-reconcile.trail.js.map +1 -0
  237. package/dist/trails/missing-visibility.trail.d.ts +24 -0
  238. package/dist/trails/missing-visibility.trail.d.ts.map +1 -0
  239. package/dist/trails/missing-visibility.trail.js +22 -0
  240. package/dist/trails/missing-visibility.trail.js.map +1 -0
  241. package/dist/trails/no-direct-impl-in-route.trail.d.ts +2 -2
  242. package/dist/trails/no-direct-impl-in-route.trail.d.ts.map +1 -1
  243. package/dist/trails/no-direct-implementation-call.trail.d.ts +2 -2
  244. package/dist/trails/no-direct-implementation-call.trail.d.ts.map +1 -1
  245. package/dist/trails/no-sync-result-assumption.trail.d.ts +2 -2
  246. package/dist/trails/no-sync-result-assumption.trail.d.ts.map +1 -1
  247. package/dist/trails/no-throw-in-detour-recover.trail.d.ts +13 -0
  248. package/dist/trails/no-throw-in-detour-recover.trail.d.ts.map +1 -0
  249. package/dist/trails/no-throw-in-detour-recover.trail.js +24 -0
  250. package/dist/trails/no-throw-in-detour-recover.trail.js.map +1 -0
  251. package/dist/trails/no-throw-in-detour-target.trail.d.ts +13 -3
  252. package/dist/trails/no-throw-in-detour-target.trail.d.ts.map +1 -1
  253. package/dist/trails/no-throw-in-implementation.trail.d.ts +2 -2
  254. package/dist/trails/no-throw-in-implementation.trail.d.ts.map +1 -1
  255. package/dist/trails/on-references-exist.trail.d.ts +24 -0
  256. package/dist/trails/on-references-exist.trail.d.ts.map +1 -0
  257. package/dist/trails/on-references-exist.trail.js +21 -0
  258. package/dist/trails/on-references-exist.trail.js.map +1 -0
  259. package/dist/trails/orphaned-signal.trail.d.ts +24 -0
  260. package/dist/trails/orphaned-signal.trail.d.ts.map +1 -0
  261. package/dist/trails/orphaned-signal.trail.js +36 -0
  262. package/dist/trails/orphaned-signal.trail.js.map +1 -0
  263. package/dist/trails/permit-governance.trail.d.ts +12 -0
  264. package/dist/trails/permit-governance.trail.d.ts.map +1 -0
  265. package/dist/trails/permit-governance.trail.js +47 -0
  266. package/dist/trails/permit-governance.trail.js.map +1 -0
  267. package/dist/trails/prefer-schema-inference.trail.d.ts +2 -2
  268. package/dist/trails/prefer-schema-inference.trail.d.ts.map +1 -1
  269. package/dist/trails/reference-exists.trail.d.ts +24 -0
  270. package/dist/trails/reference-exists.trail.d.ts.map +1 -0
  271. package/dist/trails/reference-exists.trail.js +25 -0
  272. package/dist/trails/reference-exists.trail.js.map +1 -0
  273. package/dist/trails/resource-declarations.trail.d.ts +13 -0
  274. package/dist/trails/resource-declarations.trail.d.ts.map +1 -0
  275. package/dist/trails/{provision-declarations.trail.js → resource-declarations.trail.js} +7 -7
  276. package/dist/trails/resource-declarations.trail.js.map +1 -0
  277. package/dist/trails/resource-exists.trail.d.ts +24 -0
  278. package/dist/trails/resource-exists.trail.d.ts.map +1 -0
  279. package/dist/trails/{provision-exists.trail.js → resource-exists.trail.js} +8 -8
  280. package/dist/trails/resource-exists.trail.js.map +1 -0
  281. package/dist/trails/resource-id-grammar.trail.d.ts +13 -0
  282. package/dist/trails/resource-id-grammar.trail.d.ts.map +1 -0
  283. package/dist/trails/resource-id-grammar.trail.js +38 -0
  284. package/dist/trails/resource-id-grammar.trail.js.map +1 -0
  285. package/dist/trails/run.d.ts +25 -9
  286. package/dist/trails/run.d.ts.map +1 -1
  287. package/dist/trails/run.js +63 -19
  288. package/dist/trails/run.js.map +1 -1
  289. package/dist/trails/schema.d.ts +28 -3
  290. package/dist/trails/schema.d.ts.map +1 -1
  291. package/dist/trails/schema.js +57 -4
  292. package/dist/trails/schema.js.map +1 -1
  293. package/dist/trails/unreachable-detour-shadowing.trail.d.ts +13 -0
  294. package/dist/trails/unreachable-detour-shadowing.trail.d.ts.map +1 -0
  295. package/dist/trails/unreachable-detour-shadowing.trail.js +44 -0
  296. package/dist/trails/unreachable-detour-shadowing.trail.js.map +1 -0
  297. package/dist/trails/valid-describe-refs.trail.d.ts +12 -3
  298. package/dist/trails/valid-describe-refs.trail.d.ts.map +1 -1
  299. package/dist/trails/valid-detour-contract.trail.d.ts +12 -0
  300. package/dist/trails/valid-detour-contract.trail.d.ts.map +1 -0
  301. package/dist/trails/valid-detour-contract.trail.js +66 -0
  302. package/dist/trails/valid-detour-contract.trail.js.map +1 -0
  303. package/dist/trails/valid-detour-refs.trail.d.ts +13 -3
  304. package/dist/trails/valid-detour-refs.trail.d.ts.map +1 -1
  305. package/dist/trails/warden-export-symmetry.trail.d.ts +13 -0
  306. package/dist/trails/warden-export-symmetry.trail.d.ts.map +1 -0
  307. package/dist/trails/warden-export-symmetry.trail.js +16 -0
  308. package/dist/trails/warden-export-symmetry.trail.js.map +1 -0
  309. package/dist/trails/warden-rules-use-ast.trail.d.ts +13 -0
  310. package/dist/trails/warden-rules-use-ast.trail.d.ts.map +1 -0
  311. package/dist/trails/warden-rules-use-ast.trail.js +41 -0
  312. package/dist/trails/warden-rules-use-ast.trail.js.map +1 -0
  313. package/dist/trails/wrap-rule.d.ts +16 -2
  314. package/dist/trails/wrap-rule.d.ts.map +1 -1
  315. package/dist/trails/wrap-rule.js +71 -11
  316. package/dist/trails/wrap-rule.js.map +1 -1
  317. package/package.json +7 -4
  318. package/src/__tests__/ast.test.ts +613 -0
  319. package/src/__tests__/circular-refs.test.ts +121 -0
  320. package/src/__tests__/cli.test.ts +360 -32
  321. package/src/__tests__/contour-exists.test.ts +203 -0
  322. package/src/__tests__/cross-declarations.test.ts +245 -0
  323. package/src/__tests__/dead-internal-trail.test.ts +81 -0
  324. package/src/__tests__/draft-rules-context.test.ts +150 -0
  325. package/src/__tests__/drift.test.ts +75 -5
  326. package/src/__tests__/error-mapping-completeness.test.ts +56 -0
  327. package/src/__tests__/example-valid.test.ts +101 -0
  328. package/src/__tests__/fires-declarations-param-destructure.test.ts +54 -0
  329. package/src/__tests__/fires-declarations.test.ts +652 -0
  330. package/src/__tests__/formatters.test.ts +2 -2
  331. package/src/__tests__/implementation-returns-result.test.ts +1016 -2
  332. package/src/__tests__/incomplete-accessor-for-standard-op.test.ts +337 -0
  333. package/src/__tests__/incomplete-crud.test.ts +498 -0
  334. package/src/__tests__/intent-propagation.test.ts +116 -0
  335. package/src/__tests__/missing-reconcile.test.ts +154 -0
  336. package/src/__tests__/missing-visibility.test.ts +108 -0
  337. package/src/__tests__/no-sync-result-assumption.test.ts +870 -39
  338. package/src/__tests__/no-throw-in-detour-recover.test.ts +93 -0
  339. package/src/__tests__/no-throw-in-implementation.test.ts +88 -0
  340. package/src/__tests__/on-references-exist.test.ts +151 -0
  341. package/src/__tests__/orphaned-signal.test.ts +137 -0
  342. package/src/__tests__/permit-governance.test.ts +66 -0
  343. package/src/__tests__/reference-exists.test.ts +281 -0
  344. package/src/__tests__/resource-declarations.test.ts +448 -0
  345. package/src/__tests__/resource-exists.test.ts +122 -0
  346. package/src/__tests__/resource-id-grammar.test.ts +50 -0
  347. package/src/__tests__/rules.test.ts +17 -77
  348. package/src/__tests__/topo-aware-rule.test.ts +257 -0
  349. package/src/__tests__/trails.test.ts +2 -2
  350. package/src/__tests__/unreachable-detour-shadowing.test.ts +128 -0
  351. package/src/__tests__/valid-describe-refs.test.ts +183 -0
  352. package/src/__tests__/valid-detour-contract.test.ts +86 -0
  353. package/src/__tests__/warden-export-symmetry.test.ts +251 -0
  354. package/src/__tests__/warden-rules-use-ast.test.ts +468 -0
  355. package/src/__tests__/wrap-rule.test.ts +3 -3
  356. package/src/cli.ts +458 -91
  357. package/src/draft.ts +22 -0
  358. package/src/drift.ts +63 -21
  359. package/src/formatters.ts +15 -4
  360. package/src/index.ts +62 -23
  361. package/src/rules/ast.ts +2715 -119
  362. package/src/rules/circular-refs.ts +154 -0
  363. package/src/rules/{context-no-trailhead-types.ts → context-no-surface-types.ts} +72 -12
  364. package/src/rules/contour-exists.ts +251 -0
  365. package/src/rules/contour-ids.ts +15 -0
  366. package/src/rules/cross-declarations.ts +277 -69
  367. package/src/rules/dead-internal-trail.ts +141 -0
  368. package/src/rules/draft-file-marking.ts +160 -0
  369. package/src/rules/draft-visible-debt.ts +87 -0
  370. package/src/rules/error-mapping-completeness.ts +273 -0
  371. package/src/rules/example-valid.ts +401 -0
  372. package/src/rules/fires-declarations.ts +609 -0
  373. package/src/rules/implementation-returns-result.ts +1042 -122
  374. package/src/rules/incomplete-accessor-for-standard-op.ts +315 -0
  375. package/src/rules/incomplete-crud.ts +579 -0
  376. package/src/rules/index.ts +95 -16
  377. package/src/rules/intent-propagation.ts +142 -0
  378. package/src/rules/missing-reconcile.ts +98 -0
  379. package/src/rules/missing-visibility.ts +110 -0
  380. package/src/rules/no-direct-impl-in-route.ts +0 -4
  381. package/src/rules/no-direct-implementation-call.ts +1 -1
  382. package/src/rules/no-sync-result-assumption.ts +1134 -96
  383. package/src/rules/no-throw-in-detour-recover.ts +225 -0
  384. package/src/rules/no-throw-in-implementation.ts +6 -4
  385. package/src/rules/on-references-exist.ts +194 -0
  386. package/src/rules/orphaned-signal.ts +150 -0
  387. package/src/rules/permit-governance.ts +25 -0
  388. package/src/rules/reference-exists.ts +98 -0
  389. package/src/rules/registry-names.ts +83 -0
  390. package/src/rules/{provision-declarations.ts → resource-declarations.ts} +208 -138
  391. package/src/rules/{provision-exists.ts → resource-exists.ts} +48 -51
  392. package/src/rules/resource-id-grammar.ts +65 -0
  393. package/src/rules/specs.ts +5 -1
  394. package/src/rules/types.ts +57 -4
  395. package/src/rules/unreachable-detour-shadowing.ts +375 -0
  396. package/src/rules/valid-describe-refs.ts +160 -32
  397. package/src/rules/valid-detour-contract.ts +78 -0
  398. package/src/rules/warden-export-symmetry.ts +533 -0
  399. package/src/rules/warden-rules-use-ast.ts +996 -0
  400. package/src/trails/circular-refs.trail.ts +29 -0
  401. package/src/trails/{context-no-trailhead-types.trail.ts → context-no-surface-types.trail.ts} +4 -4
  402. package/src/trails/contour-exists.trail.ts +21 -0
  403. package/src/trails/dead-internal-trail.trail.ts +26 -0
  404. package/src/trails/draft-file-marking.trail.ts +16 -0
  405. package/src/trails/draft-visible-debt.trail.ts +16 -0
  406. package/src/trails/error-mapping-completeness.trail.ts +29 -0
  407. package/src/trails/example-valid.trail.ts +25 -0
  408. package/src/trails/fires-declarations.trail.ts +22 -0
  409. package/src/trails/incomplete-accessor-for-standard-op.trail.ts +76 -0
  410. package/src/trails/incomplete-crud.trail.ts +39 -0
  411. package/src/trails/index.ts +40 -7
  412. package/src/trails/intent-propagation.trail.ts +30 -0
  413. package/src/trails/missing-reconcile.trail.ts +33 -0
  414. package/src/trails/missing-visibility.trail.ts +22 -0
  415. package/src/trails/no-throw-in-detour-recover.trail.ts +24 -0
  416. package/src/trails/on-references-exist.trail.ts +21 -0
  417. package/src/trails/orphaned-signal.trail.ts +36 -0
  418. package/src/trails/permit-governance.trail.ts +51 -0
  419. package/src/trails/reference-exists.trail.ts +25 -0
  420. package/src/trails/{provision-declarations.trail.ts → resource-declarations.trail.ts} +6 -6
  421. package/src/trails/{provision-exists.trail.ts → resource-exists.trail.ts} +7 -7
  422. package/src/trails/resource-id-grammar.trail.ts +39 -0
  423. package/src/trails/run.ts +121 -24
  424. package/src/trails/schema.ts +66 -4
  425. package/src/trails/unreachable-detour-shadowing.trail.ts +45 -0
  426. package/src/trails/valid-detour-contract.trail.ts +71 -0
  427. package/src/trails/warden-export-symmetry.trail.ts +16 -0
  428. package/src/trails/warden-rules-use-ast.trail.ts +45 -0
  429. package/src/trails/wrap-rule.ts +104 -12
  430. package/tsconfig.tests.json +10 -0
  431. package/tsconfig.tsbuildinfo +1 -1
  432. package/dist/rules/follow-declarations.d.ts +0 -13
  433. package/dist/rules/follow-declarations.d.ts.map +0 -1
  434. package/dist/rules/follow-declarations.js +0 -264
  435. package/dist/rules/follow-declarations.js.map +0 -1
  436. package/dist/rules/provision-declarations.d.ts +0 -14
  437. package/dist/rules/provision-declarations.d.ts.map +0 -1
  438. package/dist/rules/provision-declarations.js +0 -344
  439. package/dist/rules/provision-declarations.js.map +0 -1
  440. package/dist/rules/provision-exists.d.ts +0 -6
  441. package/dist/rules/provision-exists.d.ts.map +0 -1
  442. package/dist/rules/provision-exists.js +0 -89
  443. package/dist/rules/provision-exists.js.map +0 -1
  444. package/dist/rules/service-declarations.d.ts +0 -16
  445. package/dist/rules/service-declarations.d.ts.map +0 -1
  446. package/dist/rules/service-declarations.js +0 -346
  447. package/dist/rules/service-declarations.js.map +0 -1
  448. package/dist/rules/service-exists.d.ts +0 -8
  449. package/dist/rules/service-exists.d.ts.map +0 -1
  450. package/dist/rules/service-exists.js +0 -91
  451. package/dist/rules/service-exists.js.map +0 -1
  452. package/dist/trails/follow-declarations.trail.d.ts.map +0 -1
  453. package/dist/trails/follow-declarations.trail.js +0 -22
  454. package/dist/trails/follow-declarations.trail.js.map +0 -1
  455. package/dist/trails/provision-declarations.trail.d.ts.map +0 -1
  456. package/dist/trails/provision-declarations.trail.js.map +0 -1
  457. package/dist/trails/provision-exists.trail.d.ts +0 -15
  458. package/dist/trails/provision-exists.trail.d.ts.map +0 -1
  459. package/dist/trails/provision-exists.trail.js.map +0 -1
  460. package/dist/trails/service-declarations.trail.d.ts +0 -26
  461. package/dist/trails/service-declarations.trail.d.ts.map +0 -1
  462. package/dist/trails/service-declarations.trail.js +0 -27
  463. package/dist/trails/service-declarations.trail.js.map +0 -1
  464. package/dist/trails/service-exists.trail.d.ts +0 -32
  465. package/dist/trails/service-exists.trail.d.ts.map +0 -1
  466. package/dist/trails/service-exists.trail.js +0 -29
  467. package/dist/trails/service-exists.trail.js.map +0 -1
  468. package/src/__tests__/no-throw-in-detour-target.test.ts +0 -78
  469. package/src/__tests__/provision-declarations.test.ts +0 -318
  470. package/src/__tests__/provision-exists.test.ts +0 -122
  471. package/src/rules/no-throw-in-detour-target.ts +0 -150
  472. package/src/rules/valid-detour-refs.ts +0 -187
  473. package/src/trails/no-throw-in-detour-target.trail.ts +0 -20
  474. package/src/trails/valid-detour-refs.trail.ts +0 -24
@@ -0,0 +1,996 @@
1
+ /**
2
+ * Self-governance rule: warden rules must inspect the AST via the helpers in
3
+ * `./ast.ts` rather than regex-scanning raw source text. Raw-text scans
4
+ * produce false positives on string literals, template payloads, and
5
+ * docstrings — see TRL-335 and ADR-0036.
6
+ *
7
+ * Three detection families are enforced:
8
+ *
9
+ * 1. `rawScanSite` — string methods on a raw-source identifier, e.g.
10
+ * `sourceCode.split(/\n/)`, `rawText.match(...)`, `text.replace(...)`.
11
+ * 2. `regexScanSite` — regex-receiver methods consuming a raw-source
12
+ * argument, e.g. `/re/.test(sourceCode)`, `new RegExp(...).exec(text)`.
13
+ * 3. `regexConstructionSite` — constructing a regex directly from a raw
14
+ * source identifier, e.g. `new RegExp(sourceCode)`, `RegExp(rawText, 'g')`.
15
+ * Interpolating raw source into a regex constructor is the same class of
16
+ * bug as scanning with one — see TRL-345.
17
+ *
18
+ * This rule is path-anchored to this package's own `src/rules/` directory so
19
+ * it never fires against a consumer repo that happens to share the same
20
+ * folder layout. `ast.ts` itself is excluded because it IS the raw-text
21
+ * interface to the parser; `types.ts`, `index.ts`, `registry-names.ts`, and
22
+ * anything under `__tests__` are also excluded.
23
+ */
24
+ import { basename as pathBasename, dirname, resolve, sep } from 'node:path';
25
+ import { fileURLToPath } from 'node:url';
26
+
27
+ import { offsetToLine, parse, walk } from './ast.js';
28
+ import type { AstNode } from './ast.js';
29
+ import type { WardenDiagnostic, WardenRule } from './types.js';
30
+
31
+ const RULE_NAME = 'warden-rules-use-ast';
32
+
33
+ /**
34
+ * Absolute path to this package's rules directory, resolved from the rule's
35
+ * own module URL. Anchoring to the real on-disk location prevents the rule
36
+ * from firing against a foreign `packages/warden/src/rules/` in a consumer
37
+ * repository that happens to share the same folder structure.
38
+ *
39
+ * Dist-layout safeguard: when this module is bundled/transpiled to `dist/`
40
+ * (e.g. `packages/warden/dist/rules/warden-rules-use-ast.js`), the files
41
+ * being linted still live under `src/rules/`. A strict equality check
42
+ * against only the dist directory would cause the rule to silently emit
43
+ * zero diagnostics — a silent no-op. To keep the anchor robust, we compute
44
+ * a source-equivalent dir by substituting `/dist/` with `/src/` on the
45
+ * resolved path and accept either. This preserves the anti-false-positive
46
+ * guarantee from TRL-341 (we still require an exact directory match, not a
47
+ * suffix match) while surviving a future bundling change.
48
+ */
49
+ const SELF_MODULE_DIR = resolve(dirname(fileURLToPath(import.meta.url)));
50
+
51
+ /**
52
+ * Replace only the LAST occurrence of `/dist/` with `/src/`. A blanket
53
+ * `replaceAll` over-substitutes on paths that contain other `/dist/`
54
+ * segments higher up (e.g. `/home/runner/dist-artifacts/warden/dist/rules/`
55
+ * would incorrectly become `/home/runner/src-artifacts/warden/src/rules/`,
56
+ * a nonexistent directory — silently defeating the rule).
57
+ *
58
+ * Exported for unit testing. Not part of the public rule API.
59
+ */
60
+ export const replaceLastDistSegmentWithSrc = (path: string): string => {
61
+ const distSegment = `${sep}dist${sep}`;
62
+ const srcSegment = `${sep}src${sep}`;
63
+ const lastIdx = path.lastIndexOf(distSegment);
64
+ if (lastIdx === -1) {
65
+ return path;
66
+ }
67
+ return (
68
+ path.slice(0, lastIdx) +
69
+ srcSegment +
70
+ path.slice(lastIdx + distSegment.length)
71
+ );
72
+ };
73
+
74
+ const SELF_RULES_DIRS: ReadonlySet<string> = new Set(
75
+ SELF_MODULE_DIR.includes(`${sep}dist${sep}`)
76
+ ? [SELF_MODULE_DIR, replaceLastDistSegmentWithSrc(SELF_MODULE_DIR)]
77
+ : [SELF_MODULE_DIR]
78
+ );
79
+
80
+ /**
81
+ * Stems of files in `src/rules/` (and their bundled `dist/rules/` twins) that
82
+ * are NOT themselves warden rules and therefore must not be checked. `ast` is
83
+ * the raw-text interface to the parser and legitimately touches source text;
84
+ * the others are support modules without a `check()` function.
85
+ */
86
+ const EXCLUDED_STEMS: readonly string[] = [
87
+ 'ast',
88
+ 'index',
89
+ 'registry-names',
90
+ 'scan',
91
+ 'specs',
92
+ 'structure',
93
+ 'types',
94
+ ];
95
+
96
+ /**
97
+ * Both `.ts` (source layout) and `.js` (dist layout) basenames must be
98
+ * excluded so the rule stays silent when pointed at a bundled tree. The
99
+ * dist-layout `ast.js` contains the same raw-text parser entry point as
100
+ * `ast.ts` and would false-positive if scanned.
101
+ */
102
+ const EXCLUDED_BASENAMES: ReadonlySet<string> = new Set(
103
+ EXCLUDED_STEMS.flatMap((stem) => [`${stem}.ts`, `${stem}.js`])
104
+ );
105
+
106
+ const isTargetFile = (filePath: string): boolean => {
107
+ const absolute = resolve(filePath);
108
+ if (!SELF_RULES_DIRS.has(dirname(absolute))) {
109
+ return false;
110
+ }
111
+ const basename = pathBasename(absolute);
112
+ if (EXCLUDED_BASENAMES.has(basename)) {
113
+ return false;
114
+ }
115
+ if (basename.endsWith('.test.ts') || basename.endsWith('.test.js')) {
116
+ return false;
117
+ }
118
+ return true;
119
+ };
120
+
121
+ /**
122
+ * Names of the WardenRule methods that receive raw source text as their
123
+ * first parameter. The first parameter's *actual binding name* (not a fixed
124
+ * list of names) is what we track via scope analysis — see `buildSourceParamIndex`.
125
+ *
126
+ * `checkTopo` does not receive raw source text (it takes a `Topo`) so it is
127
+ * intentionally excluded.
128
+ */
129
+ const SOURCE_PARAM_METHOD_NAMES: ReadonlySet<string> = new Set([
130
+ 'check',
131
+ 'checkWithContext',
132
+ ]);
133
+
134
+ /**
135
+ * String methods that indicate raw-text *scanning* when called on a
136
+ * source-text identifier. Deliberately narrow: these are the patterns that
137
+ * produce false positives on string literals, template payloads, and
138
+ * docstrings — the regression TRL-333/TRL-334/TRL-335 fixed.
139
+ *
140
+ * Not flagged: `.slice`, `.substring`, `.indexOf`, `.includes`. These also
141
+ * take source text as input, but have legitimate AST-adjacent uses — e.g.
142
+ * `sourceCode.slice(node.start, node.end)` to recover a node's original
143
+ * text from an AST-resolved range, or `sourceCode.includes('marker')` as a
144
+ * fast-bail check before parsing. Flagging them would produce false
145
+ * positives on idiomatic rules.
146
+ */
147
+ const RAW_SCAN_METHODS: ReadonlySet<string> = new Set([
148
+ 'match',
149
+ 'matchAll',
150
+ 'replace',
151
+ 'replaceAll',
152
+ 'search',
153
+ 'split',
154
+ ]);
155
+
156
+ /**
157
+ * Methods on a regex receiver that consume a raw-text argument. Flagged
158
+ * when the argument is a raw-source identifier (`sourceCode`, `text`, etc.).
159
+ */
160
+ const REGEX_SCAN_METHODS: ReadonlySet<string> = new Set(['exec', 'test']);
161
+
162
+ const getIdentifierName = (node: AstNode | undefined): string | null => {
163
+ if (!node || node.type !== 'Identifier') {
164
+ return null;
165
+ }
166
+ const { name } = node as unknown as { name?: string };
167
+ return typeof name === 'string' ? name : null;
168
+ };
169
+
170
+ /**
171
+ * Scope-based source-param resolution. Each detector returns the
172
+ * candidate `Identifier` AST node that must resolve to the enclosing
173
+ * `check` / `checkWithContext` method's first parameter binding — i.e.
174
+ * not shadowed by any intervening `const`/`let`/`var`/param declaration.
175
+ * See `resolvesToSourceParam` for the walk.
176
+ */
177
+ interface RawScanSite {
178
+ readonly methodName: string;
179
+ readonly identifier: AstNode;
180
+ readonly identifierName: string;
181
+ readonly start: number;
182
+ }
183
+
184
+ interface MemberCallParts {
185
+ readonly object: AstNode | undefined;
186
+ readonly property: AstNode | undefined;
187
+ }
188
+
189
+ /**
190
+ * Extract the `object`/`property` of a non-computed member call, or null
191
+ * for anything else. Keeps `rawScanSite` under the max-statements budget.
192
+ */
193
+ const memberCallParts = (node: AstNode): MemberCallParts | null => {
194
+ if (node.type !== 'CallExpression') {
195
+ return null;
196
+ }
197
+ const { callee } = node as unknown as { callee?: AstNode };
198
+ if (
199
+ !callee ||
200
+ (callee.type !== 'MemberExpression' &&
201
+ callee.type !== 'StaticMemberExpression')
202
+ ) {
203
+ return null;
204
+ }
205
+ const { object, property, computed } = callee as unknown as {
206
+ object?: AstNode;
207
+ property?: AstNode;
208
+ computed?: boolean;
209
+ };
210
+ return computed ? null : { object, property };
211
+ };
212
+
213
+ const rawScanSite = (node: AstNode): RawScanSite | null => {
214
+ const parts = memberCallParts(node);
215
+ if (!parts || !parts.object) {
216
+ return null;
217
+ }
218
+ const receiver = getIdentifierName(parts.object);
219
+ if (!receiver) {
220
+ return null;
221
+ }
222
+ const methodName = getIdentifierName(parts.property);
223
+ if (!methodName || !RAW_SCAN_METHODS.has(methodName)) {
224
+ return null;
225
+ }
226
+ return {
227
+ identifier: parts.object,
228
+ identifierName: receiver,
229
+ methodName,
230
+ start: node.start,
231
+ };
232
+ };
233
+
234
+ /**
235
+ * True when `node` is a regex-producing expression: a regex literal
236
+ * (`/foo/`), `new RegExp(...)`, or a plain `RegExp(...)` call.
237
+ */
238
+ const isRegexProducer = (node: AstNode | undefined): boolean => {
239
+ if (!node) {
240
+ return false;
241
+ }
242
+ if (node.type === 'Literal' && 'regex' in node && node['regex']) {
243
+ return true;
244
+ }
245
+ if (node.type === 'RegExpLiteral') {
246
+ return true;
247
+ }
248
+ if (node.type === 'NewExpression' || node.type === 'CallExpression') {
249
+ const { callee } = node as unknown as { callee?: AstNode };
250
+ return getIdentifierName(callee) === 'RegExp';
251
+ }
252
+ return false;
253
+ };
254
+
255
+ /**
256
+ * First identifier argument of a call expression, or null. The returned
257
+ * identifier must still resolve to a tracked source-param binding (see
258
+ * `resolvesToSourceParam`) before a diagnostic is emitted — this pre-filter
259
+ * only narrows the candidate arg.
260
+ */
261
+ const firstIdentifierArgument = (
262
+ node: AstNode
263
+ ): { identifier: AstNode; name: string } | null => {
264
+ const args = (node as unknown as { arguments?: readonly AstNode[] })
265
+ .arguments;
266
+ if (!args) {
267
+ return null;
268
+ }
269
+ for (const arg of args) {
270
+ const name = getIdentifierName(arg);
271
+ if (name) {
272
+ return { identifier: arg, name };
273
+ }
274
+ }
275
+ return null;
276
+ };
277
+
278
+ const regexScanMethodName = (parts: MemberCallParts): string | null => {
279
+ if (!isRegexProducer(parts.object)) {
280
+ return null;
281
+ }
282
+ const methodName = getIdentifierName(parts.property);
283
+ if (!methodName || !REGEX_SCAN_METHODS.has(methodName)) {
284
+ return null;
285
+ }
286
+ return methodName;
287
+ };
288
+
289
+ /**
290
+ * Detects `/regex/.test(sourceCode)`, `new RegExp(...).exec(text)`, and
291
+ * similar regex-receiver calls that consume a raw-source identifier.
292
+ */
293
+ const regexScanSite = (node: AstNode): RawScanSite | null => {
294
+ const parts = memberCallParts(node);
295
+ if (!parts) {
296
+ return null;
297
+ }
298
+ const methodName = regexScanMethodName(parts);
299
+ if (!methodName) {
300
+ return null;
301
+ }
302
+ const arg = firstIdentifierArgument(node);
303
+ if (!arg) {
304
+ return null;
305
+ }
306
+ return {
307
+ identifier: arg.identifier,
308
+ identifierName: arg.name,
309
+ methodName,
310
+ start: node.start,
311
+ };
312
+ };
313
+
314
+ interface RegexConstructionSite {
315
+ readonly kind: 'new' | 'call';
316
+ readonly identifier: AstNode;
317
+ readonly identifierName: string;
318
+ readonly start: number;
319
+ }
320
+
321
+ /**
322
+ * Detects `new RegExp(sourceCode)` / `RegExp(rawText, 'g')` — constructing a
323
+ * regex from raw source text. Same anti-pattern family as
324
+ * `sourceCode.match(...)` and `/re/.test(sourceCode)`: raw source fed into a
325
+ * scanner. Fires when the callee is an `Identifier` named `RegExp` and at
326
+ * least one argument is an identifier that resolves, via scope analysis, to
327
+ * the enclosing `check` / `checkWithContext` method's first parameter.
328
+ */
329
+ const regexConstructionSite = (node: AstNode): RegexConstructionSite | null => {
330
+ if (node.type !== 'NewExpression' && node.type !== 'CallExpression') {
331
+ return null;
332
+ }
333
+ const { callee } = node as unknown as { callee?: AstNode };
334
+ if (getIdentifierName(callee) !== 'RegExp') {
335
+ return null;
336
+ }
337
+ const arg = firstIdentifierArgument(node);
338
+ if (!arg) {
339
+ return null;
340
+ }
341
+ return {
342
+ identifier: arg.identifier,
343
+ identifierName: arg.name,
344
+ kind: node.type === 'NewExpression' ? 'new' : 'call',
345
+ start: node.start,
346
+ };
347
+ };
348
+
349
+ const DIAGNOSTIC_ADVICE =
350
+ 'Warden rules must inspect the AST via packages/warden/src/rules/ast.ts helpers, not regex-scan raw source text. ' +
351
+ 'Use findStringLiterals, findTrailDefinitions, findConfigProperty, or a similar AST walker. ' +
352
+ 'Raw-text scanning produces false positives on string literals, template payloads, and docstrings — see TRL-335, ADR-0036.';
353
+
354
+ interface DetectedSite {
355
+ readonly identifier: AstNode;
356
+ readonly message: string;
357
+ readonly start: number;
358
+ }
359
+
360
+ const detectRawScan = (node: AstNode): DetectedSite | null => {
361
+ const scan = rawScanSite(node);
362
+ if (!scan) {
363
+ return null;
364
+ }
365
+ return {
366
+ identifier: scan.identifier,
367
+ message: `${RULE_NAME}: ${scan.identifierName}.${scan.methodName}(...) treats source text as a string. ${DIAGNOSTIC_ADVICE}`,
368
+ start: scan.start,
369
+ };
370
+ };
371
+
372
+ const detectRegexScan = (node: AstNode): DetectedSite | null => {
373
+ const regex = regexScanSite(node);
374
+ if (!regex) {
375
+ return null;
376
+ }
377
+ return {
378
+ identifier: regex.identifier,
379
+ message: `${RULE_NAME}: regex.${regex.methodName}(${regex.identifierName}) scans raw source text. ${DIAGNOSTIC_ADVICE}`,
380
+ start: regex.start,
381
+ };
382
+ };
383
+
384
+ const detectRegexConstruction = (node: AstNode): DetectedSite | null => {
385
+ const construction = regexConstructionSite(node);
386
+ if (!construction) {
387
+ return null;
388
+ }
389
+ const prefix = construction.kind === 'new' ? 'new RegExp' : 'RegExp';
390
+ return {
391
+ identifier: construction.identifier,
392
+ message: `${RULE_NAME}: ${prefix}(${construction.identifierName}) constructs a regex from raw source text. ${DIAGNOSTIC_ADVICE}`,
393
+ start: construction.start,
394
+ };
395
+ };
396
+
397
+ /**
398
+ * Dispatch chain for per-node detectors. Each detector tries one family in
399
+ * priority order. First match wins; descent into children still happens so
400
+ * nested offenses (e.g. a regex scan inside a callback passed to a raw-text
401
+ * scan) are still caught.
402
+ */
403
+ const DETECTORS: readonly ((node: AstNode) => DetectedSite | null)[] = [
404
+ detectRawScan,
405
+ detectRegexScan,
406
+ detectRegexConstruction,
407
+ ];
408
+
409
+ const detectSite = (node: AstNode): DetectedSite | null => {
410
+ for (const detector of DETECTORS) {
411
+ const site = detector(node);
412
+ if (site) {
413
+ return site;
414
+ }
415
+ }
416
+ return null;
417
+ };
418
+
419
+ // ---------------------------------------------------------------------------
420
+ // Scope analysis (Option A — parameter-origin tracking).
421
+ //
422
+ // The pre-TRL-346 detectors gated on identifier spelling alone (a fixed set
423
+ // like `sourceCode`, `text`, `source`, `rawText`). That over-fires on
424
+ // unrelated locals that happen to share one of those names, and under-fires
425
+ // when a rule author picks a different name for the source parameter.
426
+ //
427
+ // Option A walks the AST with a scope stack, records the first parameter of
428
+ // any `check` / `checkWithContext` method (its *actual binding name*), and
429
+ // only flags a call site when the candidate identifier still refers to that
430
+ // exact binding — i.e. no intervening `const`/`let`/`var`/param has
431
+ // shadowed it.
432
+ // ---------------------------------------------------------------------------
433
+
434
+ interface Scope {
435
+ readonly declaredNames: ReadonlySet<string>;
436
+ readonly sourceParamName: string | null;
437
+ }
438
+
439
+ /**
440
+ * Walk inner→outer. The first scope that declares `name` is the binding; the
441
+ * identifier resolves to a tracked source-param only when that declaring
442
+ * scope's `sourceParamName` matches. An identifier with no declaring scope
443
+ * (e.g. a free variable) is not a source-param binding.
444
+ */
445
+ const resolvesToSourceParam = (
446
+ name: string,
447
+ scopes: readonly Scope[]
448
+ ): boolean => {
449
+ for (let i = scopes.length - 1; i >= 0; i -= 1) {
450
+ const scope = scopes[i];
451
+ if (scope && scope.declaredNames.has(name)) {
452
+ return scope.sourceParamName === name;
453
+ }
454
+ }
455
+ return false;
456
+ };
457
+
458
+ const FUNCTION_NODE_TYPES: ReadonlySet<string> = new Set([
459
+ 'ArrowFunctionExpression',
460
+ 'FunctionDeclaration',
461
+ 'FunctionExpression',
462
+ ]);
463
+
464
+ /**
465
+ * Name of a function-like node when it is a recognized WardenRule method
466
+ * (`check` or `checkWithContext`). Returns null otherwise.
467
+ *
468
+ * Handles three shapes:
469
+ * - object-literal method shorthand: `{ check(sc) { ... } }`
470
+ * (Property with `method: true`, or MethodDefinition)
471
+ * - arrow/function property: `{ check: (sc) => { ... } }`
472
+ * - top-level function declaration: `function check(sc) { ... }`
473
+ *
474
+ * The context-to-function link is resolved by the caller via the
475
+ * `methodFunctionStarts` map: we pre-walk the AST once to map the start
476
+ * offset of every recognized function to its method name, then consult the
477
+ * map when the scope walker enters that function.
478
+ */
479
+ const methodNameFromKey = (key: AstNode | undefined): string | null => {
480
+ if (!key) {
481
+ return null;
482
+ }
483
+ if (key.type === 'Identifier') {
484
+ return (key as unknown as { name?: string }).name ?? null;
485
+ }
486
+ // String-literal keys like `'check': (sc) => { ... }`.
487
+ if (
488
+ (key.type === 'Literal' || key.type === 'StringLiteral') &&
489
+ typeof (key as unknown as { value?: unknown }).value === 'string'
490
+ ) {
491
+ return (key as unknown as { value: string }).value;
492
+ }
493
+ return null;
494
+ };
495
+
496
+ const firstParamIdentifierName = (fn: AstNode): string | null => {
497
+ const { params } = fn as unknown as { params?: readonly AstNode[] };
498
+ const [first] = params ?? [];
499
+ if (!first) {
500
+ return null;
501
+ }
502
+ if (first.type === 'Identifier') {
503
+ return getIdentifierName(first);
504
+ }
505
+ if (first.type === 'AssignmentPattern') {
506
+ const { left } = first as unknown as { left?: AstNode };
507
+ return left?.type === 'Identifier' ? getIdentifierName(left) : null;
508
+ }
509
+ return null;
510
+ };
511
+
512
+ /**
513
+ * Collect start offsets of function-like AST nodes that represent the body
514
+ * of a recognized WardenRule source-receiving method. Value is the declared
515
+ * first-parameter name, used as `sourceParamName` when the scope walker
516
+ * pushes that function's scope.
517
+ */
518
+ const methodPropertyFunction = (
519
+ node: AstNode
520
+ ): { fn: AstNode; name: string } | null => {
521
+ const { key, value } = node as unknown as {
522
+ key?: AstNode;
523
+ value?: AstNode;
524
+ };
525
+ const name = methodNameFromKey(key);
526
+ if (!name || !value || !FUNCTION_NODE_TYPES.has(value.type)) {
527
+ return null;
528
+ }
529
+ return SOURCE_PARAM_METHOD_NAMES.has(name) ? { fn: value, name } : null;
530
+ };
531
+
532
+ const namedFunctionDeclaration = (
533
+ node: AstNode
534
+ ): { fn: AstNode; name: string } | null => {
535
+ const name = getIdentifierName((node as unknown as { id?: AstNode }).id);
536
+ if (!name || !SOURCE_PARAM_METHOD_NAMES.has(name)) {
537
+ return null;
538
+ }
539
+ return { fn: node, name };
540
+ };
541
+
542
+ const recognizedMethodFunction = (
543
+ node: AstNode
544
+ ): { fn: AstNode; name: string } | null => {
545
+ if (node.type === 'Property' || node.type === 'MethodDefinition') {
546
+ return methodPropertyFunction(node);
547
+ }
548
+ if (node.type === 'FunctionDeclaration') {
549
+ return namedFunctionDeclaration(node);
550
+ }
551
+ return null;
552
+ };
553
+
554
+ const buildSourceParamIndex = (ast: AstNode): ReadonlyMap<number, string> => {
555
+ const index = new Map<number, string>();
556
+ walk(ast, (node) => {
557
+ const recognized = recognizedMethodFunction(node);
558
+ if (!recognized) {
559
+ return;
560
+ }
561
+ const paramName = firstParamIdentifierName(recognized.fn);
562
+ if (paramName) {
563
+ index.set(recognized.fn.start, paramName);
564
+ }
565
+ });
566
+ return index;
567
+ };
568
+
569
+ /**
570
+ * Collect identifier names introduced at this scope by
571
+ * `const`/`let`/`var`/function declarations or function params. We only
572
+ * inspect direct children — nested block statements and nested functions
573
+ * have their own scopes.
574
+ */
575
+ const expandObjectPatternProperty = (property: AstNode): readonly AstNode[] => {
576
+ if (property.type === 'Property') {
577
+ const { value } = property as unknown as { value?: AstNode };
578
+ return value ? [value] : [];
579
+ }
580
+ if (property.type === 'RestElement') {
581
+ const { argument } = property as unknown as { argument?: AstNode };
582
+ return argument ? [argument] : [];
583
+ }
584
+ return [];
585
+ };
586
+
587
+ const PATTERN_EXPANDERS: Record<string, (p: AstNode) => readonly AstNode[]> = {
588
+ ArrayPattern: (pattern) => {
589
+ const elements =
590
+ (pattern as unknown as { elements?: readonly (AstNode | null)[] })
591
+ .elements ?? [];
592
+ return elements.filter((el): el is AstNode => el !== null);
593
+ },
594
+ AssignmentPattern: (pattern) => {
595
+ const { left } = pattern as unknown as { left?: AstNode };
596
+ return left ? [left] : [];
597
+ },
598
+ ObjectPattern: (pattern) => {
599
+ const properties =
600
+ (pattern as unknown as { properties?: readonly AstNode[] }).properties ??
601
+ [];
602
+ return properties.flatMap(expandObjectPatternProperty);
603
+ },
604
+ RestElement: (pattern) => {
605
+ const { argument } = pattern as unknown as { argument?: AstNode };
606
+ return argument ? [argument] : [];
607
+ },
608
+ };
609
+
610
+ /**
611
+ * Collect identifier names introduced by a binding pattern (function
612
+ * parameter, destructuring target, etc.). Iterative worklist over
613
+ * {@link PATTERN_EXPANDERS}: each expander yields one level of child
614
+ * patterns, and the loop bottoms out at `Identifier` nodes. The iterative
615
+ * shape avoids mutual recursion so every helper stays under the
616
+ * `max-statements` budget.
617
+ */
618
+ const visitPatternNode = (
619
+ current: AstNode,
620
+ into: Set<string>,
621
+ worklist: AstNode[]
622
+ ): void => {
623
+ if (current.type === 'Identifier') {
624
+ const name = getIdentifierName(current);
625
+ if (name) {
626
+ into.add(name);
627
+ }
628
+ return;
629
+ }
630
+ const expand = PATTERN_EXPANDERS[current.type];
631
+ if (expand) {
632
+ worklist.push(...expand(current));
633
+ }
634
+ };
635
+
636
+ const collectBindingIdsFromPattern = (
637
+ pattern: AstNode | undefined,
638
+ into: Set<string>
639
+ ): void => {
640
+ if (!pattern) {
641
+ return;
642
+ }
643
+ const worklist: AstNode[] = [pattern];
644
+ while (worklist.length > 0) {
645
+ const current = worklist.pop();
646
+ if (current) {
647
+ visitPatternNode(current, into, worklist);
648
+ }
649
+ }
650
+ };
651
+
652
+ interface FunctionScopeBindingsEx {
653
+ /** Param names exactly — used to identify the source-param binding. */
654
+ readonly paramNames: Set<string>;
655
+ /** Hoisted `var` names inside the function body. May overlap with params. */
656
+ readonly hoistedVarNames: Set<string>;
657
+ /** Combined set of declared names visible in this function scope. */
658
+ readonly declaredNames: Set<string>;
659
+ }
660
+
661
+ const collectParamBindingsFromFunction = (fn: AstNode): Set<string> => {
662
+ const paramNames = new Set<string>();
663
+ const params =
664
+ (fn as unknown as { params?: readonly AstNode[] }).params ?? [];
665
+ for (const param of params) {
666
+ collectBindingIdsFromPattern(param, paramNames);
667
+ }
668
+ return paramNames;
669
+ };
670
+
671
+ const collectHoistedVarsFromFunctionBody = (fn: AstNode): Set<string> => {
672
+ const hoistedVarNames = new Set<string>();
673
+ const { body } = fn as unknown as { body?: AstNode };
674
+ if (body && typeof body === 'object' && (body as AstNode).type) {
675
+ // eslint-disable-next-line no-use-before-define
676
+ collectHoistedVarBindings(body, hoistedVarNames);
677
+ }
678
+ return hoistedVarNames;
679
+ };
680
+
681
+ /**
682
+ * Combine param names and body-level hoisted `var` names into a single
683
+ * declared-names set, while keeping the two subsets addressable. The
684
+ * hoisted set is kept separate so the scope walker can tell whether a
685
+ * source-param identity has been overwritten by a same-named hoisted local —
686
+ * a shadow that would otherwise be invisible because both names live in the
687
+ * same function-scope binding set.
688
+ */
689
+ const collectFunctionScopeBindingsEx = (
690
+ fn: AstNode
691
+ ): FunctionScopeBindingsEx => {
692
+ const paramNames = collectParamBindingsFromFunction(fn);
693
+ const hoistedVarNames = collectHoistedVarsFromFunctionBody(fn);
694
+ const declaredNames = new Set<string>(paramNames);
695
+ for (const name of hoistedVarNames) {
696
+ declaredNames.add(name);
697
+ }
698
+ return { declaredNames, hoistedVarNames, paramNames };
699
+ };
700
+
701
+ const addVariableDeclarationNames = (
702
+ stmt: AstNode,
703
+ names: Set<string>
704
+ ): void => {
705
+ const declarations =
706
+ (stmt as unknown as { declarations?: readonly AstNode[] }).declarations ??
707
+ [];
708
+ for (const decl of declarations) {
709
+ collectBindingIdsFromPattern(
710
+ (decl as unknown as { id?: AstNode }).id,
711
+ names
712
+ );
713
+ }
714
+ };
715
+
716
+ const isVarVariableDeclaration = (stmt: AstNode): boolean =>
717
+ stmt.type === 'VariableDeclaration' &&
718
+ (stmt as unknown as { kind?: string }).kind === 'var';
719
+
720
+ /**
721
+ * True when `node` owns its own VariableEnvironment and therefore stops `var`
722
+ * hoisting from crossing into the enclosing function/program scope.
723
+ */
724
+ const ownsVariableEnvironmentForHoisting = (node: AstNode): boolean =>
725
+ FUNCTION_NODE_TYPES.has(node.type) || node.type === 'StaticBlock';
726
+
727
+ /**
728
+ * Collect `var` declarations that hoist to the nearest function (or program)
729
+ * scope from anywhere inside `root`, without crossing a nested function or
730
+ * static-block boundary. Mirrors the hoisting semantics used by
731
+ * {@link ./no-sync-result-assumption.ts} so `if (cond) { var sourceCode = ... }`
732
+ * inside a `check()` body correctly shadows the method's first parameter.
733
+ */
734
+ const collectHoistedVarBindings = (root: AstNode, out: Set<string>): void => {
735
+ const walkVar = (node: AstNode, isRoot: boolean): void => {
736
+ if (!isRoot && ownsVariableEnvironmentForHoisting(node)) {
737
+ return;
738
+ }
739
+ if (isVarVariableDeclaration(node)) {
740
+ addVariableDeclarationNames(node, out);
741
+ }
742
+ for (const val of Object.values(node)) {
743
+ if (Array.isArray(val)) {
744
+ for (const item of val) {
745
+ if (item && typeof item === 'object' && (item as AstNode).type) {
746
+ walkVar(item as AstNode, false);
747
+ }
748
+ }
749
+ } else if (val && typeof val === 'object' && (val as AstNode).type) {
750
+ walkVar(val as AstNode, false);
751
+ }
752
+ }
753
+ };
754
+ walkVar(root, true);
755
+ };
756
+
757
+ const addFunctionDeclarationName = (
758
+ stmt: AstNode,
759
+ names: Set<string>
760
+ ): void => {
761
+ const name = getIdentifierName((stmt as unknown as { id?: AstNode }).id);
762
+ if (name) {
763
+ names.add(name);
764
+ }
765
+ };
766
+
767
+ const collectBlockDeclarationNames = (block: AstNode): Set<string> => {
768
+ const names = new Set<string>();
769
+ const body = (block as unknown as { body?: readonly AstNode[] }).body ?? [];
770
+ for (const stmt of body) {
771
+ // `var` is function-scoped, not block-scoped — hoisted into the nearest
772
+ // enclosing function (or program) scope by
773
+ // {@link collectHoistedVarBindings}. Registering it here would make
774
+ // `if (cond) { var x = ... }` look block-local and fail to shadow a
775
+ // same-named outer binding when the write escapes the block.
776
+ if (
777
+ stmt.type === 'VariableDeclaration' &&
778
+ !isVarVariableDeclaration(stmt)
779
+ ) {
780
+ addVariableDeclarationNames(stmt, names);
781
+ } else if (stmt.type === 'FunctionDeclaration') {
782
+ addFunctionDeclarationName(stmt, names);
783
+ }
784
+ }
785
+ return names;
786
+ };
787
+
788
+ /**
789
+ * Collect the names a `CatchClause` parameter introduces into its body. The
790
+ * catch clause has its own binding scope distinct from the surrounding block;
791
+ * without this, `try {} catch (sourceCode) { sourceCode.split(...) }` would
792
+ * resolve `sourceCode` to the enclosing `check()` parameter and fire.
793
+ */
794
+ const collectCatchClauseDeclarationNames = (node: AstNode): Set<string> => {
795
+ const names = new Set<string>();
796
+ const { param } = node as unknown as { param?: AstNode };
797
+ collectBindingIdsFromPattern(param, names);
798
+ return names;
799
+ };
800
+
801
+ interface ScopeWalkContext {
802
+ readonly diagnostics: WardenDiagnostic[];
803
+ readonly filePath: string;
804
+ readonly methodFunctionStarts: ReadonlyMap<number, string>;
805
+ readonly sourceCode: string;
806
+ }
807
+
808
+ const recordDiagnostic = (ctx: ScopeWalkContext, site: DetectedSite): void => {
809
+ ctx.diagnostics.push({
810
+ filePath: ctx.filePath,
811
+ line: offsetToLine(ctx.sourceCode, site.start),
812
+ message: site.message,
813
+ rule: RULE_NAME,
814
+ severity: 'error' as const,
815
+ });
816
+ };
817
+
818
+ /**
819
+ * Emit a diagnostic if `node` is a detected site whose candidate identifier
820
+ * resolves (via the current scope stack) to a tracked source-param binding.
821
+ */
822
+ const maybeRecordDetection = (
823
+ node: AstNode,
824
+ scopes: readonly Scope[],
825
+ ctx: ScopeWalkContext
826
+ ): void => {
827
+ const site = detectSite(node);
828
+ if (!site) {
829
+ return;
830
+ }
831
+ const name = getIdentifierName(site.identifier);
832
+ if (name && resolvesToSourceParam(name, scopes)) {
833
+ recordDiagnostic(ctx, site);
834
+ }
835
+ };
836
+
837
+ /**
838
+ * Resolve the effective source-param name for a function scope. A hoisted
839
+ * `var` with the same name as the source param overwrites the param's slot
840
+ * in the function's VariableEnvironment, so the enclosing identifier no
841
+ * longer resolves to the raw-source binding.
842
+ */
843
+ const resolveScopeSourceParamName = (
844
+ methodParamName: string | null,
845
+ hoistedVarNames: ReadonlySet<string>
846
+ ): string | null =>
847
+ methodParamName && hoistedVarNames.has(methodParamName)
848
+ ? null
849
+ : methodParamName;
850
+
851
+ const pushFunctionScope = (
852
+ node: AstNode,
853
+ ctx: ScopeWalkContext,
854
+ scopes: Scope[]
855
+ ): void => {
856
+ const methodParamName = ctx.methodFunctionStarts.get(node.start) ?? null;
857
+ const { declaredNames, hoistedVarNames } =
858
+ collectFunctionScopeBindingsEx(node);
859
+ scopes.push({
860
+ declaredNames,
861
+ sourceParamName: resolveScopeSourceParamName(
862
+ methodParamName,
863
+ hoistedVarNames
864
+ ),
865
+ });
866
+ };
867
+
868
+ /** Collector for scope frames that carry no source-param identity. */
869
+ const SIMPLE_SCOPE_COLLECTORS: Record<
870
+ string,
871
+ (node: AstNode) => ReadonlySet<string>
872
+ > = {
873
+ BlockStatement: collectBlockDeclarationNames,
874
+ CatchClause: collectCatchClauseDeclarationNames,
875
+ };
876
+
877
+ /**
878
+ * Push the scope a function node introduces, or null when the node is not
879
+ * scope-introducing. Returning a dispose function keeps `visitWithScopes`
880
+ * small and keeps the scope stack strictly paired.
881
+ */
882
+ const enterScopeForNode = (
883
+ node: AstNode,
884
+ ctx: ScopeWalkContext,
885
+ scopes: Scope[]
886
+ ): boolean => {
887
+ if (FUNCTION_NODE_TYPES.has(node.type)) {
888
+ pushFunctionScope(node, ctx, scopes);
889
+ return true;
890
+ }
891
+ const collector = SIMPLE_SCOPE_COLLECTORS[node.type];
892
+ if (collector) {
893
+ scopes.push({
894
+ declaredNames: collector(node),
895
+ sourceParamName: null,
896
+ });
897
+ return true;
898
+ }
899
+ return false;
900
+ };
901
+
902
+ interface EnterFrame {
903
+ kind: 'enter';
904
+ node: AstNode;
905
+ }
906
+ type WalkFrame = EnterFrame | { kind: 'leave'; pushed: boolean };
907
+
908
+ /**
909
+ * Build "enter" frames for every AST child of `node`. Returned reversed so
910
+ * consumers can `Array#push(...frames)` onto a stack and still visit
911
+ * children in source order via `Array#pop`.
912
+ */
913
+ const collectChildFrames = (node: AstNode): readonly EnterFrame[] => {
914
+ const frames: EnterFrame[] = [];
915
+ for (const value of Object.values(node)) {
916
+ if (Array.isArray(value)) {
917
+ for (const item of value) {
918
+ if (item && typeof item === 'object' && (item as AstNode).type) {
919
+ frames.push({ kind: 'enter', node: item as AstNode });
920
+ }
921
+ }
922
+ continue;
923
+ }
924
+ if (value && typeof value === 'object' && (value as AstNode).type) {
925
+ frames.push({ kind: 'enter', node: value as AstNode });
926
+ }
927
+ }
928
+ return frames.toReversed();
929
+ };
930
+
931
+ const processFrame = (
932
+ frame: WalkFrame,
933
+ scopes: Scope[],
934
+ ctx: ScopeWalkContext,
935
+ stack: WalkFrame[]
936
+ ): void => {
937
+ if (frame.kind === 'leave') {
938
+ if (frame.pushed) {
939
+ scopes.pop();
940
+ }
941
+ return;
942
+ }
943
+ const { node } = frame;
944
+ maybeRecordDetection(node, scopes, ctx);
945
+ const pushed = enterScopeForNode(node, ctx, scopes);
946
+ stack.push({ kind: 'leave', pushed });
947
+ stack.push(...collectChildFrames(node));
948
+ };
949
+
950
+ /**
951
+ * Scope-aware AST walker. Iterative DFS: each enter frame schedules the
952
+ * node's children in source order and queues a matching leave frame so
953
+ * scope pops stay balanced with their pushes.
954
+ */
955
+ const analyze = (
956
+ sourceCode: string,
957
+ filePath: string,
958
+ ast: AstNode
959
+ ): readonly WardenDiagnostic[] => {
960
+ const ctx: ScopeWalkContext = {
961
+ diagnostics: [],
962
+ filePath,
963
+ methodFunctionStarts: buildSourceParamIndex(ast),
964
+ sourceCode,
965
+ };
966
+ const scopes: Scope[] = [];
967
+ const stack: WalkFrame[] = [{ kind: 'enter', node: ast }];
968
+ while (stack.length > 0) {
969
+ const frame = stack.pop();
970
+ if (frame) {
971
+ processFrame(frame, scopes, ctx, stack);
972
+ }
973
+ }
974
+ return ctx.diagnostics;
975
+ };
976
+
977
+ /**
978
+ * Warden rule enforcing that warden rules themselves walk the AST rather than
979
+ * regex-scan raw source text.
980
+ */
981
+ export const wardenRulesUseAst: WardenRule = {
982
+ check(sourceCode: string, filePath: string): readonly WardenDiagnostic[] {
983
+ if (!isTargetFile(filePath)) {
984
+ return [];
985
+ }
986
+ const ast = parse(filePath, sourceCode);
987
+ if (!ast) {
988
+ return [];
989
+ }
990
+ return analyze(sourceCode, filePath, ast);
991
+ },
992
+ description:
993
+ 'Enforces that warden rules inspect the AST via packages/warden/src/rules/ast.ts helpers rather than regex-scanning raw source text.',
994
+ name: RULE_NAME,
995
+ severity: 'error',
996
+ };