@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
@@ -5,30 +5,20 @@
5
5
  * every return statement returns Result.ok(), Result.err(), ctx.cross(),
6
6
  * or a tracked Result-typed variable.
7
7
  */
8
- import { findBlazeBodies, findTrailDefinitions, offsetToLine, parse, walk, } from './ast.js';
8
+ import { dirname, isAbsolute, resolve } from 'node:path';
9
+ import { existsSync, readFileSync } from 'node:fs';
10
+ import { collectScopeFrameBindings, findBlazeBodies, findTrailDefinitions, getMemberExpression, identifierName, offsetToLine, parse, walk, walkWithScopes, } from './ast.js';
9
11
  import { isTestFile } from './scan.js';
10
12
  // ---------------------------------------------------------------------------
11
13
  // Member expression helpers
12
14
  // ---------------------------------------------------------------------------
13
- /** Extract object.property names from a MemberExpression callee. */
14
- const extractMemberNames = (callee) => {
15
- const obj = callee.object;
16
- const prop = callee.property;
17
- const objName = obj?.type === 'Identifier'
18
- ? obj.name
19
- : undefined;
20
- const propName = prop?.type === 'Identifier'
21
- ? prop.name
22
- : undefined;
23
- return { objName, propName };
24
- };
25
- const isMemberExpression = (callee) => callee.type === 'StaticMemberExpression' ||
26
- callee.type === 'MemberExpression';
27
15
  const isResultMemberCall = (callee) => {
28
- if (!isMemberExpression(callee)) {
16
+ const member = getMemberExpression(callee);
17
+ if (!member) {
29
18
  return false;
30
19
  }
31
- const { objName, propName } = extractMemberNames(callee);
20
+ const objName = identifierName(member.object) ?? undefined;
21
+ const propName = identifierName(member.property) ?? undefined;
32
22
  if (objName === 'Result' && (propName === 'ok' || propName === 'err')) {
33
23
  return true;
34
24
  }
@@ -55,8 +45,34 @@ const isResultExpression = (node) => {
55
45
  }
56
46
  return false;
57
47
  };
48
+ /**
49
+ * Check whether a namespace-member call like `ns.helper(...)` resolves to a
50
+ * known Result helper.
51
+ *
52
+ * When a non-empty `scopes` stack is provided, the namespace binding must not
53
+ * be shadowed by a parameter or local declaration in any enclosing scope at
54
+ * the call site. Without this check, any local `ns` (e.g. a blaze parameter
55
+ * named `ns`, or `const ns = ...` inside the body) would be misread as the
56
+ * module-scope namespace import.
57
+ */
58
+ const isNamespaceHelperMemberCall = (callee, namespaceHelpers, scopes = []) => {
59
+ const member = getMemberExpression(callee);
60
+ if (!member) {
61
+ return false;
62
+ }
63
+ const objName = identifierName(member.object) ?? undefined;
64
+ const propName = identifierName(member.property) ?? undefined;
65
+ if (!(objName && propName)) {
66
+ return false;
67
+ }
68
+ // Nearest binding is a local, not the namespace import.
69
+ if (scopes.some((scope) => scope.has(objName))) {
70
+ return false;
71
+ }
72
+ return namespaceHelpers.get(objName)?.has(propName) ?? false;
73
+ };
58
74
  /** Check if a node is a call to a known Result-returning helper. */
59
- const isHelperCall = (node, helperNames) => {
75
+ const isHelperCall = (node, helperNames, namespaceHelpers = new Map(), scopes = []) => {
60
76
  const target = node.type === 'AwaitExpression'
61
77
  ? (node.argument ?? null)
62
78
  : node;
@@ -68,7 +84,9 @@ const isHelperCall = (node, helperNames) => {
68
84
  const { name } = callee;
69
85
  return helperNames.has(name);
70
86
  }
71
- return false;
87
+ return callee
88
+ ? isNamespaceHelperMemberCall(callee, namespaceHelpers, scopes)
89
+ : false;
72
90
  };
73
91
  /** Unwrap an optional AwaitExpression to get the inner identifier name. */
74
92
  const resolveIdentifierName = (node) => {
@@ -84,11 +102,11 @@ const resolveIdentifierName = (node) => {
84
102
  return null;
85
103
  };
86
104
  /** Check if a return argument is an allowed Result value. */
87
- const isAllowedReturnArgument = (argument, helperNames, resultVars) => {
105
+ const isAllowedReturnArgument = (argument, helperNames, resultVars, namespaceHelpers, scopes = []) => {
88
106
  if (isResultExpression(argument)) {
89
107
  return true;
90
108
  }
91
- if (isHelperCall(argument, helperNames)) {
109
+ if (isHelperCall(argument, helperNames, namespaceHelpers, scopes)) {
92
110
  return true;
93
111
  }
94
112
  const varName = resolveIdentifierName(argument);
@@ -109,58 +127,13 @@ const trackResultVariable = (node, resultVars) => {
109
127
  }
110
128
  };
111
129
  // ---------------------------------------------------------------------------
112
- // Shallow walk (stops at nested function boundaries)
113
- // ---------------------------------------------------------------------------
114
- const FUNCTION_BOUNDARY_TYPES = new Set([
115
- 'ArrowFunctionExpression',
116
- 'FunctionExpression',
117
- 'FunctionDeclaration',
118
- ]);
119
- /** Check if a value is a function-boundary AST node that should not be recursed into. */
120
- const isFunctionBoundary = (val) => !!val &&
121
- typeof val === 'object' &&
122
- FUNCTION_BOUNDARY_TYPES.has(val.type);
123
- /** Recurse into a single AST property value, skipping function boundaries. */
124
- const visitValue = (val, visit, recurse) => {
125
- if (Array.isArray(val)) {
126
- for (const item of val) {
127
- if (!isFunctionBoundary(item)) {
128
- recurse(item, visit);
129
- }
130
- }
131
- }
132
- else if (val &&
133
- typeof val === 'object' &&
134
- val.type &&
135
- !isFunctionBoundary(val)) {
136
- recurse(val, visit);
137
- }
138
- };
139
- /**
140
- * Walk an AST node tree without recursing into nested function bodies.
141
- *
142
- * This ensures that return statements inside `.map()`, `.filter()`, `.then()`
143
- * callbacks etc. are not mistakenly checked as implementation-level returns.
144
- */
145
- const walkShallow = (node, visit) => {
146
- if (!node || typeof node !== 'object') {
147
- return;
148
- }
149
- const n = node;
150
- if (n.type) {
151
- visit(n);
152
- }
153
- for (const val of Object.values(n)) {
154
- visitValue(val, visit, walkShallow);
155
- }
156
- };
157
- // ---------------------------------------------------------------------------
158
130
  // Return statement checking
159
131
  // ---------------------------------------------------------------------------
160
132
  /** Check return statements in a block body for non-Result returns. */
161
- const checkReturnStatements = (blockBody, trailInfo, filePath, sourceCode, helperNames, diagnostics) => {
133
+ const checkReturnStatements = (blockBody, trailInfo, filePath, sourceCode, helperNames, namespaceHelpers, diagnostics, implScope = new Set()) => {
162
134
  const resultVars = new Set();
163
- walkShallow(blockBody, (node) => {
135
+ const initialScopes = implScope.size > 0 ? [implScope] : [];
136
+ walkWithScopes(blockBody, (node, currentScopes) => {
164
137
  if (node.type === 'VariableDeclarator') {
165
138
  trackResultVariable(node, resultVars);
166
139
  }
@@ -172,7 +145,7 @@ const checkReturnStatements = (blockBody, trailInfo, filePath, sourceCode, helpe
172
145
  if (!argument) {
173
146
  return;
174
147
  }
175
- if (isAllowedReturnArgument(argument, helperNames, resultVars)) {
148
+ if (isAllowedReturnArgument(argument, helperNames, resultVars, namespaceHelpers, currentScopes)) {
176
149
  return;
177
150
  }
178
151
  diagnostics.push({
@@ -182,7 +155,7 @@ const checkReturnStatements = (blockBody, trailInfo, filePath, sourceCode, helpe
182
155
  rule: 'implementation-returns-result',
183
156
  severity: 'error',
184
157
  });
185
- });
158
+ }, { initialScopes, stopAtNestedFunctions: true });
186
159
  };
187
160
  // ---------------------------------------------------------------------------
188
161
  // Result helper name collection
@@ -221,18 +194,606 @@ const collectResultHelperNames = (ast, sourceCode) => {
221
194
  return names;
222
195
  };
223
196
  // ---------------------------------------------------------------------------
197
+ // Imported Result helper resolution
198
+ // ---------------------------------------------------------------------------
199
+ /**
200
+ * Per-target-file cache of exported Result-helper names keyed by the absolute
201
+ * target path. Saves re-parsing when multiple rule invocations resolve the
202
+ * same file during a single warden run.
203
+ *
204
+ * @remarks
205
+ * Long-running processes calling `implementationReturnsResult.check` after
206
+ * source files change (e.g. watch mode, editor language servers) should call
207
+ * `clearImplementationReturnsResultCache()` between runs to avoid returning
208
+ * stale helper-name sets. The cache is intentionally not auto-invalidated per
209
+ * invocation — that would defeat its purpose within a single warden run.
210
+ */
211
+ const targetFileResultExportCache = new Map();
212
+ /**
213
+ * Clear the module-level cache used by the `implementation-returns-result`
214
+ * rule to remember which exported names on a target file carry a `Result<...>`
215
+ * return annotation.
216
+ *
217
+ * Call this between runs in long-lived processes where the set of Trails
218
+ * source files may have changed on disk since the last check.
219
+ */
220
+ export const clearImplementationReturnsResultCache = () => {
221
+ targetFileResultExportCache.clear();
222
+ };
223
+ const getImportSourceValue = (node) => {
224
+ const sourceNode = node.source;
225
+ const sourceValue = sourceNode
226
+ ? sourceNode.value
227
+ : undefined;
228
+ return typeof sourceValue === 'string' ? sourceValue : null;
229
+ };
230
+ const extractIdentifierName = (node) => node?.type === 'Identifier'
231
+ ? (node.name ?? null)
232
+ : null;
233
+ const buildDefaultImportBinding = (specifier, source) => {
234
+ const { local } = specifier;
235
+ const localName = extractIdentifierName(local);
236
+ if (!localName) {
237
+ return null;
238
+ }
239
+ return { importedName: 'default', localName, source };
240
+ };
241
+ const buildNamedImportBinding = (specifier, source) => {
242
+ const { local, imported } = specifier;
243
+ const localName = extractIdentifierName(local);
244
+ const importedName = extractIdentifierName(imported) ?? localName;
245
+ if (!(localName && importedName)) {
246
+ return null;
247
+ }
248
+ return { importedName, localName, source };
249
+ };
250
+ /**
251
+ * @remarks
252
+ * `import foo from './bar.js'` is treated as a re-export of `default` so the
253
+ * target file's `export default` declaration is considered as a potential
254
+ * Result helper. `import * as ns from './bar.js'` is handled separately by
255
+ * `collectNamespaceHelperImports`, which maps the namespace binding to the
256
+ * target's exported Result-helper names so `ns.helper(...)` member calls are
257
+ * recognized.
258
+ */
259
+ const buildImportBinding = (specifier, source) => {
260
+ if (specifier.type === 'ImportDefaultSpecifier') {
261
+ return buildDefaultImportBinding(specifier, source);
262
+ }
263
+ if (specifier.type === 'ImportSpecifier') {
264
+ return buildNamedImportBinding(specifier, source);
265
+ }
266
+ return null;
267
+ };
268
+ const collectBindingsFromImportDeclaration = (node) => {
269
+ const source = getImportSourceValue(node);
270
+ if (!source) {
271
+ return [];
272
+ }
273
+ const specifiers = node['specifiers'] ?? [];
274
+ return specifiers.flatMap((specifier) => {
275
+ const binding = buildImportBinding(specifier, source);
276
+ return binding ? [binding] : [];
277
+ });
278
+ };
279
+ /** Collect `import { foo as bar } from './...'` bindings keyed by local name. */
280
+ const collectResolvableImports = (ast) => {
281
+ const imports = [];
282
+ walk(ast, (node) => {
283
+ if (node.type === 'ImportDeclaration') {
284
+ imports.push(...collectBindingsFromImportDeclaration(node));
285
+ }
286
+ });
287
+ return imports;
288
+ };
289
+ /**
290
+ * Resolve a relative import source specifier to an absolute on-disk file path,
291
+ * or null when the source is not a relative path we can resolve locally.
292
+ *
293
+ * Handles `.js` -> `.ts` rewriting (the convention in this repo), plain `.ts`
294
+ * imports, and extensionless paths.
295
+ */
296
+ const buildResolutionCandidates = (resolved) => {
297
+ if (resolved.endsWith('.ts') || resolved.endsWith('.tsx')) {
298
+ return [resolved];
299
+ }
300
+ if (resolved.endsWith('.js')) {
301
+ return [
302
+ resolved.replace(/\.js$/, '.ts'),
303
+ resolved.replace(/\.js$/, '.tsx'),
304
+ resolved,
305
+ ];
306
+ }
307
+ if (resolved.endsWith('.jsx')) {
308
+ return [resolved.replace(/\.jsx$/, '.tsx'), resolved];
309
+ }
310
+ return [`${resolved}.ts`, `${resolved}.tsx`];
311
+ };
312
+ const resolveRelativeImportPath = (source, fromFile) => {
313
+ if (!(source.startsWith('./') || source.startsWith('../'))) {
314
+ return null;
315
+ }
316
+ const baseDir = isAbsolute(fromFile)
317
+ ? dirname(fromFile)
318
+ : dirname(resolve(fromFile));
319
+ const resolved = resolve(baseDir, source);
320
+ return (buildResolutionCandidates(resolved).find((candidate) => existsSync(candidate)) ?? null);
321
+ };
322
+ /** Extract the declaration wrapped by an ExportNamedDeclaration, if any. */
323
+ const getExportedDeclaration = (node) => {
324
+ if (node.type !== 'ExportNamedDeclaration') {
325
+ return null;
326
+ }
327
+ const decl = node.declaration;
328
+ return decl ?? null;
329
+ };
330
+ const addExportedVariableResultHelper = (decl, source, collected) => {
331
+ const declarations = decl['declarations'] ?? [];
332
+ for (const declarator of declarations) {
333
+ const { id, init } = declarator;
334
+ const name = extractIdentifierName(id);
335
+ if (name &&
336
+ init &&
337
+ isFunctionLikeExpression(init) &&
338
+ hasResultReturnType(init, source)) {
339
+ collected.add(name);
340
+ }
341
+ }
342
+ };
343
+ const addExportedFunctionResultHelper = (decl, source, collected) => {
344
+ const name = extractIdentifierName(decl.id);
345
+ if (name && hasResultReturnType(decl, source)) {
346
+ collected.add(name);
347
+ }
348
+ };
349
+ const indexVariableDeclarationInto = (decl, index) => {
350
+ const declarators = decl['declarations'] ?? [];
351
+ for (const declarator of declarators) {
352
+ const { id, init } = declarator;
353
+ const name = extractIdentifierName(id);
354
+ if (name && init && isFunctionLikeExpression(init)) {
355
+ index.set(name, init);
356
+ }
357
+ }
358
+ };
359
+ const indexFunctionDeclarationInto = (decl, index) => {
360
+ const name = extractIdentifierName(decl.id);
361
+ if (name) {
362
+ index.set(name, decl);
363
+ }
364
+ };
365
+ const indexDeclarationInto = (decl, index) => {
366
+ if (!decl) {
367
+ return;
368
+ }
369
+ if (decl.type === 'VariableDeclaration') {
370
+ indexVariableDeclarationInto(decl, index);
371
+ }
372
+ else if (decl.type === 'FunctionDeclaration') {
373
+ indexFunctionDeclarationInto(decl, index);
374
+ }
375
+ };
376
+ const indexBodyNodeInto = (node, index) => {
377
+ if (node.type === 'ExportNamedDeclaration') {
378
+ indexDeclarationInto(getExportedDeclaration(node), index);
379
+ return;
380
+ }
381
+ indexDeclarationInto(node, index);
382
+ };
383
+ const indexLocalDeclarations = (ast) => {
384
+ const index = new Map();
385
+ const program = ast;
386
+ const bodyNodes = program.body ?? [];
387
+ for (const node of bodyNodes) {
388
+ indexBodyNodeInto(node, index);
389
+ }
390
+ return index;
391
+ };
392
+ const getSpecifierNameNode = (spec, key) => {
393
+ const node = spec[key];
394
+ if (!node) {
395
+ return null;
396
+ }
397
+ if (node.type === 'Identifier') {
398
+ return node.name ?? null;
399
+ }
400
+ // Support string-literal specifiers (`export { "default" as X }`, etc).
401
+ const { value } = node;
402
+ return typeof value === 'string' ? value : null;
403
+ };
404
+ const buildExportSpecifierInfo = (spec) => {
405
+ if (spec.type !== 'ExportSpecifier') {
406
+ return null;
407
+ }
408
+ const localName = getSpecifierNameNode(spec, 'local');
409
+ const exportedName = getSpecifierNameNode(spec, 'exported') ?? localName;
410
+ if (!(localName && exportedName)) {
411
+ return null;
412
+ }
413
+ return {
414
+ exportedName,
415
+ isDefault: localName === 'default',
416
+ localName,
417
+ };
418
+ };
419
+ const getExportDefaultDeclaration = (ast) => {
420
+ const program = ast;
421
+ const bodyNodes = program.body ?? [];
422
+ for (const node of bodyNodes) {
423
+ if (node.type === 'ExportDefaultDeclaration') {
424
+ const decl = node.declaration;
425
+ return decl ?? null;
426
+ }
427
+ }
428
+ return null;
429
+ };
430
+ // Bounded recursion: one transitive hop through `export { ... } from`.
431
+ const MAX_RERESOLVE_DEPTH = 1;
432
+ /** Check whether a local declaration node has a `Result<...>` return annotation. */
433
+ const isResultHelperDeclaration = (declarationNode, source) => {
434
+ if (!declarationNode) {
435
+ return false;
436
+ }
437
+ if (isFunctionLikeExpression(declarationNode)) {
438
+ return hasResultReturnType(declarationNode, source);
439
+ }
440
+ if (declarationNode.type === 'FunctionDeclaration') {
441
+ return hasResultReturnType(declarationNode, source);
442
+ }
443
+ return false;
444
+ };
445
+ /** Resolve an `export default ...` declaration, following one identifier hop. */
446
+ const checkDefaultDeclarationIsResultHelper = (defaultDecl, targetSource, targetLocalDeclarations) => {
447
+ if (isResultHelperDeclaration(defaultDecl, targetSource)) {
448
+ return true;
449
+ }
450
+ if (defaultDecl.type === 'Identifier') {
451
+ const name = extractIdentifierName(defaultDecl);
452
+ const referenced = name ? targetLocalDeclarations.get(name) : undefined;
453
+ return isResultHelperDeclaration(referenced, targetSource);
454
+ }
455
+ return false;
456
+ };
457
+ const loadTargetFile = (targetPath) => {
458
+ try {
459
+ const source = readFileSync(targetPath, 'utf8');
460
+ const ast = parse(targetPath, source);
461
+ if (!ast) {
462
+ return null;
463
+ }
464
+ return {
465
+ ast,
466
+ localDeclarations: indexLocalDeclarations(ast),
467
+ source,
468
+ };
469
+ }
470
+ catch {
471
+ return null;
472
+ }
473
+ };
474
+ const applyDefaultSpecifier = (info, loadedTarget, collected) => {
475
+ if (!loadedTarget) {
476
+ return;
477
+ }
478
+ const defaultDecl = getExportDefaultDeclaration(loadedTarget.ast);
479
+ if (!defaultDecl) {
480
+ return;
481
+ }
482
+ if (checkDefaultDeclarationIsResultHelper(defaultDecl, loadedTarget.source, loadedTarget.localDeclarations)) {
483
+ collected.add(info.exportedName);
484
+ }
485
+ };
486
+ const applySpecifierInfo = (info, ctx, collected) => {
487
+ if (info.isDefault) {
488
+ applyDefaultSpecifier(info, ctx.loadedTarget, collected);
489
+ return;
490
+ }
491
+ if (ctx.downstreamResultNames.has(info.localName)) {
492
+ collected.add(info.exportedName);
493
+ }
494
+ };
495
+ const resolveReExportTargetPath = (node, targetPath, visited, depth) => {
496
+ if (depth >= MAX_RERESOLVE_DEPTH) {
497
+ return null;
498
+ }
499
+ const reSource = getImportSourceValue(node);
500
+ if (!reSource) {
501
+ return null;
502
+ }
503
+ const reTargetPath = resolveRelativeImportPath(reSource, targetPath);
504
+ if (!reTargetPath || visited.has(reTargetPath)) {
505
+ return null;
506
+ }
507
+ return reTargetPath;
508
+ };
509
+ const buildReExportContext = (reTargetPath, specifierInfos, targetPath, visited, depth) => {
510
+ const needsDefault = specifierInfos.some((info) => info.isDefault);
511
+ // Load once when the default specifier branch needs the target AST; the
512
+ // same loaded object is threaded into the downstream walk so it isn't
513
+ // read and parsed a second time within this check() call.
514
+ const loadedTarget = needsDefault ? loadTargetFile(reTargetPath) : null;
515
+ // eslint-disable-next-line no-use-before-define
516
+ const downstreamResultNames = collectTargetExportedResultHelperNames(reTargetPath, visited, targetPath, depth + 1, loadedTarget);
517
+ return {
518
+ downstreamResultNames,
519
+ loadedTarget,
520
+ };
521
+ };
522
+ /**
523
+ * Resolve a re-export with source (`export { ... } from './x.js'`) by pulling
524
+ * the matching names off the target file, honoring aliases and `default`.
525
+ */
526
+ const resolveReExportWithSource = (node, specifiers, targetPath, visited, depth, collected) => {
527
+ const reTargetPath = resolveReExportTargetPath(node, targetPath, visited, depth);
528
+ if (!reTargetPath) {
529
+ return;
530
+ }
531
+ const specifierInfos = specifiers.flatMap((spec) => {
532
+ const info = buildExportSpecifierInfo(spec);
533
+ return info ? [info] : [];
534
+ });
535
+ const ctx = buildReExportContext(reTargetPath, specifierInfos, targetPath, visited, depth);
536
+ for (const info of specifierInfos) {
537
+ applySpecifierInfo(info, ctx, collected);
538
+ }
539
+ };
540
+ /** Resolve a specifier-only re-export (`export { helper };`) against same-file declarations. */
541
+ const resolveReExportWithoutSource = (specifiers, localDeclarations, source, collected) => {
542
+ for (const spec of specifiers) {
543
+ const info = buildExportSpecifierInfo(spec);
544
+ if (!info || info.isDefault) {
545
+ continue;
546
+ }
547
+ if (isResultHelperDeclaration(localDeclarations.get(info.localName), source)) {
548
+ collected.add(info.exportedName);
549
+ }
550
+ }
551
+ };
552
+ const processInlineExportedDeclaration = (exportedDecl, source, collected) => {
553
+ if (exportedDecl.type === 'VariableDeclaration') {
554
+ addExportedVariableResultHelper(exportedDecl, source, collected);
555
+ return true;
556
+ }
557
+ if (exportedDecl.type === 'FunctionDeclaration') {
558
+ addExportedFunctionResultHelper(exportedDecl, source, collected);
559
+ return true;
560
+ }
561
+ return false;
562
+ };
563
+ const processExportNamedDeclaration = (node, source, targetPath, visited, depth, localDeclarations, collected) => {
564
+ const exportedDecl = getExportedDeclaration(node);
565
+ if (exportedDecl &&
566
+ processInlineExportedDeclaration(exportedDecl, source, collected)) {
567
+ return;
568
+ }
569
+ const specifiers = node['specifiers'] ?? [];
570
+ if (specifiers.length === 0) {
571
+ return;
572
+ }
573
+ if (getImportSourceValue(node)) {
574
+ resolveReExportWithSource(node, specifiers, targetPath, visited, depth, collected);
575
+ return;
576
+ }
577
+ resolveReExportWithoutSource(specifiers, localDeclarations, source, collected);
578
+ };
579
+ const processExportDefaultDeclaration = (node, source, localDeclarations, collected) => {
580
+ const defaultDecl = node
581
+ .declaration;
582
+ if (!defaultDecl) {
583
+ return;
584
+ }
585
+ if (checkDefaultDeclarationIsResultHelper(defaultDecl, source, localDeclarations)) {
586
+ collected.add('default');
587
+ }
588
+ };
589
+ const collectExportedResultHelpersFromAst = (ast, source, targetPath, visited, depth, preloadedLocalDeclarations = null) => {
590
+ const collected = new Set();
591
+ // Reuse the preloaded declaration index when available (e.g., threaded in
592
+ // from `loadTargetFile`) to avoid re-walking the same AST.
593
+ const localDeclarations = preloadedLocalDeclarations ?? indexLocalDeclarations(ast);
594
+ const program = ast;
595
+ const bodyNodes = program.body ?? [];
596
+ for (const node of bodyNodes) {
597
+ if (node.type === 'ExportNamedDeclaration') {
598
+ processExportNamedDeclaration(node, source, targetPath, visited, depth, localDeclarations, collected);
599
+ }
600
+ else if (node.type === 'ExportDefaultDeclaration') {
601
+ processExportDefaultDeclaration(node, source, localDeclarations, collected);
602
+ }
603
+ else if (node.type === 'ExportAllDeclaration') {
604
+ // eslint-disable-next-line no-use-before-define
605
+ processExportAllDeclaration(node, targetPath, visited, depth, collected);
606
+ }
607
+ }
608
+ return collected;
609
+ };
610
+ /**
611
+ * Handle `export * from './x.js'` by recursing into the target module and
612
+ * unioning its exported Result-helper names. Type-only re-exports
613
+ * (`export type * from '...'`) contribute nothing. Bounded by
614
+ * `MAX_RERESOLVE_DEPTH` and the visited-set cycle guard shared with the
615
+ * specifier re-export path.
616
+ */
617
+ const processExportAllDeclaration = (node, targetPath, visited, depth, collected) => {
618
+ const { exportKind } = node;
619
+ if (exportKind === 'type') {
620
+ return;
621
+ }
622
+ const reTargetPath = resolveReExportTargetPath(node, targetPath, visited, depth);
623
+ if (!reTargetPath) {
624
+ return;
625
+ }
626
+ // eslint-disable-next-line no-use-before-define
627
+ const downstream = collectTargetExportedResultHelperNames(reTargetPath, visited, targetPath, depth + 1);
628
+ // `export * from` does NOT re-export the default binding, so we union
629
+ // only the named Result helpers from the downstream module.
630
+ for (const name of downstream) {
631
+ if (name !== 'default') {
632
+ collected.add(name);
633
+ }
634
+ }
635
+ };
636
+ const parseTargetResultHelperNames = (targetPath, visited, depth, preloaded = null) => {
637
+ const loaded = preloaded ?? loadTargetFile(targetPath);
638
+ if (!loaded) {
639
+ return new Set();
640
+ }
641
+ return collectExportedResultHelpersFromAst(loaded.ast, loaded.source, targetPath, visited, depth, loaded.localDeclarations);
642
+ };
643
+ const buildVisitedPathSet = (parentVisited, targetPath, parentPath) => {
644
+ const seeds = [...parentVisited, targetPath];
645
+ if (parentPath) {
646
+ seeds.push(parentPath);
647
+ }
648
+ return new Set(seeds);
649
+ };
650
+ /**
651
+ * Collect the set of exported names from a target file whose declaration has
652
+ * an explicit `Result<...>` / `Promise<Result<...>>` return annotation.
653
+ *
654
+ * Uses a visited-set on the recursion path to guard against `export { ... }
655
+ * from` import cycles between files. Depth is capped at a single transitive
656
+ * hop (see `MAX_RERESOLVE_DEPTH`) — deeper chains silently fall back.
657
+ */
658
+ // Only the direct-import path (no parents visited) is safe to cache: the
659
+ // computed set is a function of (targetPath, parentVisited), and
660
+ // cycle-truncated results from transitive walks must not bleed into later
661
+ // direct lookups. See PR #204 review.
662
+ const readCachedResultExports = (targetPath, parentVisited) => {
663
+ if (parentVisited.size !== 0) {
664
+ return;
665
+ }
666
+ return targetFileResultExportCache.get(targetPath);
667
+ };
668
+ // biome-ignore lint/style/useConst: declared as a function so hoisting lets `buildReExportContext` (a const declared earlier) reference it before its textual definition
669
+ // eslint-disable-next-line func-style, no-use-before-define
670
+ function collectTargetExportedResultHelperNames(targetPath, parentVisited = new Set(), parentPath, depth = 0, preloaded = null) {
671
+ if (parentVisited.has(targetPath)) {
672
+ return new Set();
673
+ }
674
+ const cached = readCachedResultExports(targetPath, parentVisited);
675
+ if (cached) {
676
+ return cached;
677
+ }
678
+ const visited = buildVisitedPathSet(parentVisited, targetPath, parentPath);
679
+ const names = parseTargetResultHelperNames(targetPath, visited, depth, preloaded);
680
+ if (parentVisited.size === 0) {
681
+ targetFileResultExportCache.set(targetPath, names);
682
+ }
683
+ return names;
684
+ }
685
+ /**
686
+ * Extend a local-helper-name set with Result-returning helpers imported from
687
+ * relative modules. Falls back silently on any resolution/parse failure.
688
+ */
689
+ const collectImportedResultHelperNames = (ast, filePath) => {
690
+ const names = new Set();
691
+ for (const binding of collectResolvableImports(ast)) {
692
+ const targetPath = resolveRelativeImportPath(binding.source, filePath);
693
+ if (!targetPath) {
694
+ continue;
695
+ }
696
+ const exportedResultNames = collectTargetExportedResultHelperNames(targetPath);
697
+ if (exportedResultNames.has(binding.importedName)) {
698
+ names.add(binding.localName);
699
+ }
700
+ }
701
+ return names;
702
+ };
703
+ /** Extract a namespace specifier's local name if it is a namespace import. */
704
+ const getNamespaceLocalName = (spec) => {
705
+ if (spec.type !== 'ImportNamespaceSpecifier') {
706
+ return null;
707
+ }
708
+ const { local } = spec;
709
+ return extractIdentifierName(local);
710
+ };
711
+ /**
712
+ * Resolve a single namespace specifier to (localName, resultHelperNames), or
713
+ * null when the specifier is not a resolvable namespace import.
714
+ *
715
+ * We intentionally record the namespace even when the target file exports no
716
+ * Result helpers (empty set). `isNamespaceHelperMemberCall` can then identify
717
+ * `ns.anything()` as a namespace member call against a non-Result-helper
718
+ * target — which correctly falls through to the general return-value
719
+ * diagnostic path. Dropping the entry would misclassify the call as a
720
+ * *non-namespace* member call and skip the namespace-shadowing scope check.
721
+ */
722
+ const resolveNamespaceSpecifier = (spec, source, filePath) => {
723
+ const localName = getNamespaceLocalName(spec);
724
+ if (!localName) {
725
+ return null;
726
+ }
727
+ const targetPath = resolveRelativeImportPath(source, filePath);
728
+ if (!targetPath) {
729
+ return null;
730
+ }
731
+ const names = collectTargetExportedResultHelperNames(targetPath);
732
+ return { localName, names };
733
+ };
734
+ /** Extract namespace helper entries from a single ImportDeclaration node. */
735
+ const namespaceEntriesFromImport = (node, filePath) => {
736
+ const source = getImportSourceValue(node);
737
+ if (!source) {
738
+ return [];
739
+ }
740
+ const specifiers = node['specifiers'] ?? [];
741
+ return specifiers.flatMap((spec) => {
742
+ const entry = resolveNamespaceSpecifier(spec, source, filePath);
743
+ return entry ? [entry] : [];
744
+ });
745
+ };
746
+ /**
747
+ * Collect `import * as ns from './foo.js'` bindings and map each local
748
+ * namespace name to the set of Result-returning helper names exported by the
749
+ * resolved target module. Returns an empty map if no namespace imports are
750
+ * found or none resolve to local files.
751
+ */
752
+ const collectNamespaceHelperImports = (ast, filePath) => {
753
+ const map = new Map();
754
+ walk(ast, (node) => {
755
+ if (node.type !== 'ImportDeclaration') {
756
+ return;
757
+ }
758
+ for (const { localName, names } of namespaceEntriesFromImport(node, filePath)) {
759
+ map.set(localName, names);
760
+ }
761
+ });
762
+ return map;
763
+ };
764
+ /**
765
+ * Combine same-file helper names with helpers imported from relative modules.
766
+ */
767
+ const collectAllResultHelperNames = (ast, sourceCode, filePath) => {
768
+ const local = collectResultHelperNames(ast, sourceCode);
769
+ const imported = collectImportedResultHelperNames(ast, filePath);
770
+ if (imported.size === 0) {
771
+ return local;
772
+ }
773
+ const merged = new Set(local);
774
+ for (const name of imported) {
775
+ merged.add(name);
776
+ }
777
+ return merged;
778
+ };
779
+ // ---------------------------------------------------------------------------
224
780
  // Per-implementation checking
225
781
  // ---------------------------------------------------------------------------
226
- const checkImplementation = (implValue, info, filePath, sourceCode, helperNames, diagnostics) => {
782
+ const checkImplementation = (implValue, info, filePath, sourceCode, helperNames, namespaceHelpers, diagnostics) => {
227
783
  const fnBody = implValue.body;
228
784
  if (!fnBody) {
229
785
  return;
230
786
  }
787
+ // Seed analysis with the implementation's own bindings so parameter names
788
+ // and hoisted vars shadow namespace imports in both block and concise bodies.
789
+ const implScope = collectScopeFrameBindings(implValue);
231
790
  if (fnBody.type === 'BlockStatement' || fnBody.type === 'FunctionBody') {
232
- checkReturnStatements(fnBody, info, filePath, sourceCode, helperNames, diagnostics);
791
+ checkReturnStatements(fnBody, info, filePath, sourceCode, helperNames, namespaceHelpers, diagnostics, implScope);
233
792
  return;
234
793
  }
235
- if (!isResultExpression(fnBody) && !isHelperCall(fnBody, helperNames)) {
794
+ const conciseScopes = implScope.size > 0 ? [implScope] : [];
795
+ if (!isResultExpression(fnBody) &&
796
+ !isHelperCall(fnBody, helperNames, namespaceHelpers, conciseScopes)) {
236
797
  diagnostics.push({
237
798
  filePath,
238
799
  line: offsetToLine(sourceCode, implValue.start),
@@ -247,11 +808,12 @@ const checkImplementation = (implValue, info, filePath, sourceCode, helperNames,
247
808
  // ---------------------------------------------------------------------------
248
809
  const checkAllDefinitions = (ast, filePath, sourceCode) => {
249
810
  const diagnostics = [];
250
- const helperNames = collectResultHelperNames(ast, sourceCode);
811
+ const helperNames = collectAllResultHelperNames(ast, sourceCode, filePath);
812
+ const namespaceHelpers = collectNamespaceHelperImports(ast, filePath);
251
813
  for (const def of findTrailDefinitions(ast)) {
252
814
  const info = { id: def.id, label: 'Trail' };
253
815
  for (const implValue of findBlazeBodies(def.config)) {
254
- checkImplementation(implValue, info, filePath, sourceCode, helperNames, diagnostics);
816
+ checkImplementation(implValue, info, filePath, sourceCode, helperNames, namespaceHelpers, diagnostics);
255
817
  }
256
818
  }
257
819
  return diagnostics;