@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
@@ -6,57 +6,34 @@
6
6
  * or a tracked Result-typed variable.
7
7
  */
8
8
 
9
+ import { dirname, isAbsolute, resolve } from 'node:path';
10
+ import { existsSync, readFileSync } from 'node:fs';
11
+ import type { AstNode } from './ast.js';
9
12
  import {
13
+ collectScopeFrameBindings,
10
14
  findBlazeBodies,
11
15
  findTrailDefinitions,
16
+ getMemberExpression,
17
+ identifierName,
12
18
  offsetToLine,
13
19
  parse,
14
20
  walk,
21
+ walkWithScopes,
15
22
  } from './ast.js';
16
23
  import { isTestFile } from './scan.js';
17
24
  import type { WardenDiagnostic, WardenRule } from './types.js';
18
25
 
19
- // ---------------------------------------------------------------------------
20
- // Types
21
- // ---------------------------------------------------------------------------
22
-
23
- interface AstNode {
24
- readonly type: string;
25
- readonly start: number;
26
- readonly end: number;
27
- readonly [key: string]: unknown;
28
- }
29
-
30
26
  // ---------------------------------------------------------------------------
31
27
  // Member expression helpers
32
28
  // ---------------------------------------------------------------------------
33
29
 
34
- /** Extract object.property names from a MemberExpression callee. */
35
- const extractMemberNames = (
36
- callee: AstNode
37
- ): { objName: string | undefined; propName: string | undefined } => {
38
- const obj = (callee as unknown as { object?: AstNode }).object;
39
- const prop = (callee as unknown as { property?: AstNode }).property;
40
- const objName =
41
- obj?.type === 'Identifier'
42
- ? (obj as unknown as { name: string }).name
43
- : undefined;
44
- const propName =
45
- prop?.type === 'Identifier'
46
- ? (prop as unknown as { name: string }).name
47
- : undefined;
48
- return { objName, propName };
49
- };
50
-
51
- const isMemberExpression = (callee: AstNode): boolean =>
52
- callee.type === 'StaticMemberExpression' ||
53
- callee.type === 'MemberExpression';
54
-
55
30
  const isResultMemberCall = (callee: AstNode): boolean => {
56
- if (!isMemberExpression(callee)) {
31
+ const member = getMemberExpression(callee);
32
+ if (!member) {
57
33
  return false;
58
34
  }
59
- const { objName, propName } = extractMemberNames(callee);
35
+ const objName = identifierName(member.object) ?? undefined;
36
+ const propName = identifierName(member.property) ?? undefined;
60
37
  if (objName === 'Result' && (propName === 'ok' || propName === 'err')) {
61
38
  return true;
62
39
  }
@@ -88,10 +65,46 @@ const isResultExpression = (node: AstNode): boolean => {
88
65
  return false;
89
66
  };
90
67
 
68
+ /** Map of namespace-import local name to the set of Result-helper names exported by the target module. */
69
+ type NamespaceHelperMap = ReadonlyMap<string, ReadonlySet<string>>;
70
+
71
+ /**
72
+ * Check whether a namespace-member call like `ns.helper(...)` resolves to a
73
+ * known Result helper.
74
+ *
75
+ * When a non-empty `scopes` stack is provided, the namespace binding must not
76
+ * be shadowed by a parameter or local declaration in any enclosing scope at
77
+ * the call site. Without this check, any local `ns` (e.g. a blaze parameter
78
+ * named `ns`, or `const ns = ...` inside the body) would be misread as the
79
+ * module-scope namespace import.
80
+ */
81
+ const isNamespaceHelperMemberCall = (
82
+ callee: AstNode,
83
+ namespaceHelpers: NamespaceHelperMap,
84
+ scopes: readonly ReadonlySet<string>[] = []
85
+ ): boolean => {
86
+ const member = getMemberExpression(callee);
87
+ if (!member) {
88
+ return false;
89
+ }
90
+ const objName = identifierName(member.object) ?? undefined;
91
+ const propName = identifierName(member.property) ?? undefined;
92
+ if (!(objName && propName)) {
93
+ return false;
94
+ }
95
+ // Nearest binding is a local, not the namespace import.
96
+ if (scopes.some((scope) => scope.has(objName))) {
97
+ return false;
98
+ }
99
+ return namespaceHelpers.get(objName)?.has(propName) ?? false;
100
+ };
101
+
91
102
  /** Check if a node is a call to a known Result-returning helper. */
92
103
  const isHelperCall = (
93
104
  node: AstNode,
94
- helperNames: ReadonlySet<string>
105
+ helperNames: ReadonlySet<string>,
106
+ namespaceHelpers: NamespaceHelperMap = new Map(),
107
+ scopes: readonly ReadonlySet<string>[] = []
95
108
  ): boolean => {
96
109
  const target =
97
110
  node.type === 'AwaitExpression'
@@ -108,7 +121,9 @@ const isHelperCall = (
108
121
  return helperNames.has(name);
109
122
  }
110
123
 
111
- return false;
124
+ return callee
125
+ ? isNamespaceHelperMemberCall(callee, namespaceHelpers, scopes)
126
+ : false;
112
127
  };
113
128
 
114
129
  /** Unwrap an optional AwaitExpression to get the inner identifier name. */
@@ -129,12 +144,14 @@ const resolveIdentifierName = (node: AstNode): string | null => {
129
144
  const isAllowedReturnArgument = (
130
145
  argument: AstNode,
131
146
  helperNames: ReadonlySet<string>,
132
- resultVars: ReadonlySet<string>
147
+ resultVars: ReadonlySet<string>,
148
+ namespaceHelpers: NamespaceHelperMap,
149
+ scopes: readonly ReadonlySet<string>[] = []
133
150
  ): boolean => {
134
151
  if (isResultExpression(argument)) {
135
152
  return true;
136
153
  }
137
- if (isHelperCall(argument, helperNames)) {
154
+ if (isHelperCall(argument, helperNames, namespaceHelpers, scopes)) {
138
155
  return true;
139
156
  }
140
157
 
@@ -158,63 +175,6 @@ const trackResultVariable = (node: AstNode, resultVars: Set<string>): void => {
158
175
  }
159
176
  };
160
177
 
161
- // ---------------------------------------------------------------------------
162
- // Shallow walk (stops at nested function boundaries)
163
- // ---------------------------------------------------------------------------
164
-
165
- const FUNCTION_BOUNDARY_TYPES = new Set([
166
- 'ArrowFunctionExpression',
167
- 'FunctionExpression',
168
- 'FunctionDeclaration',
169
- ]);
170
-
171
- /** Check if a value is a function-boundary AST node that should not be recursed into. */
172
- const isFunctionBoundary = (val: unknown): boolean =>
173
- !!val &&
174
- typeof val === 'object' &&
175
- FUNCTION_BOUNDARY_TYPES.has((val as AstNode).type);
176
-
177
- /** Recurse into a single AST property value, skipping function boundaries. */
178
- const visitValue = (
179
- val: unknown,
180
- visit: (node: AstNode) => void,
181
- recurse: (node: unknown, visit: (node: AstNode) => void) => void
182
- ): void => {
183
- if (Array.isArray(val)) {
184
- for (const item of val) {
185
- if (!isFunctionBoundary(item)) {
186
- recurse(item, visit);
187
- }
188
- }
189
- } else if (
190
- val &&
191
- typeof val === 'object' &&
192
- (val as AstNode).type &&
193
- !isFunctionBoundary(val)
194
- ) {
195
- recurse(val, visit);
196
- }
197
- };
198
-
199
- /**
200
- * Walk an AST node tree without recursing into nested function bodies.
201
- *
202
- * This ensures that return statements inside `.map()`, `.filter()`, `.then()`
203
- * callbacks etc. are not mistakenly checked as implementation-level returns.
204
- */
205
- const walkShallow = (node: unknown, visit: (node: AstNode) => void): void => {
206
- if (!node || typeof node !== 'object') {
207
- return;
208
- }
209
- const n = node as AstNode;
210
- if (n.type) {
211
- visit(n);
212
- }
213
- for (const val of Object.values(n)) {
214
- visitValue(val, visit, walkShallow);
215
- }
216
- };
217
-
218
178
  // ---------------------------------------------------------------------------
219
179
  // Return statement checking
220
180
  // ---------------------------------------------------------------------------
@@ -226,37 +186,52 @@ const checkReturnStatements = (
226
186
  filePath: string,
227
187
  sourceCode: string,
228
188
  helperNames: ReadonlySet<string>,
229
- diagnostics: WardenDiagnostic[]
189
+ namespaceHelpers: NamespaceHelperMap,
190
+ diagnostics: WardenDiagnostic[],
191
+ implScope: ReadonlySet<string> = new Set<string>()
230
192
  ): void => {
231
193
  const resultVars = new Set<string>();
194
+ const initialScopes = implScope.size > 0 ? [implScope] : [];
232
195
 
233
- walkShallow(blockBody, (node) => {
234
- if (node.type === 'VariableDeclarator') {
235
- trackResultVariable(node, resultVars);
236
- }
196
+ walkWithScopes(
197
+ blockBody,
198
+ (node, currentScopes) => {
199
+ if (node.type === 'VariableDeclarator') {
200
+ trackResultVariable(node, resultVars);
201
+ }
237
202
 
238
- if (node.type !== 'ReturnStatement') {
239
- return;
240
- }
203
+ if (node.type !== 'ReturnStatement') {
204
+ return;
205
+ }
241
206
 
242
- const { argument } = node as unknown as { argument?: AstNode };
243
- // Bare return — not a value return
244
- if (!argument) {
245
- return;
246
- }
207
+ const { argument } = node as unknown as { argument?: AstNode };
208
+ // Bare return — not a value return
209
+ if (!argument) {
210
+ return;
211
+ }
247
212
 
248
- if (isAllowedReturnArgument(argument, helperNames, resultVars)) {
249
- return;
250
- }
213
+ if (
214
+ isAllowedReturnArgument(
215
+ argument,
216
+ helperNames,
217
+ resultVars,
218
+ namespaceHelpers,
219
+ currentScopes
220
+ )
221
+ ) {
222
+ return;
223
+ }
251
224
 
252
- diagnostics.push({
253
- filePath,
254
- line: offsetToLine(sourceCode, node.start),
255
- message: `${trailInfo.label} "${trailInfo.id}" implementation must return Result.ok(...) or Result.err(...), not a raw value.`,
256
- rule: 'implementation-returns-result',
257
- severity: 'error',
258
- });
259
- });
225
+ diagnostics.push({
226
+ filePath,
227
+ line: offsetToLine(sourceCode, node.start),
228
+ message: `${trailInfo.label} "${trailInfo.id}" implementation must return Result.ok(...) or Result.err(...), not a raw value.`,
229
+ rule: 'implementation-returns-result',
230
+ severity: 'error',
231
+ });
232
+ },
233
+ { initialScopes, stopAtNestedFunctions: true }
234
+ );
260
235
  };
261
236
 
262
237
  // ---------------------------------------------------------------------------
@@ -308,6 +283,937 @@ const collectResultHelperNames = (
308
283
  return names;
309
284
  };
310
285
 
286
+ // ---------------------------------------------------------------------------
287
+ // Imported Result helper resolution
288
+ // ---------------------------------------------------------------------------
289
+
290
+ /**
291
+ * Per-target-file cache of exported Result-helper names keyed by the absolute
292
+ * target path. Saves re-parsing when multiple rule invocations resolve the
293
+ * same file during a single warden run.
294
+ *
295
+ * @remarks
296
+ * Long-running processes calling `implementationReturnsResult.check` after
297
+ * source files change (e.g. watch mode, editor language servers) should call
298
+ * `clearImplementationReturnsResultCache()` between runs to avoid returning
299
+ * stale helper-name sets. The cache is intentionally not auto-invalidated per
300
+ * invocation — that would defeat its purpose within a single warden run.
301
+ */
302
+ const targetFileResultExportCache = new Map<string, ReadonlySet<string>>();
303
+
304
+ /**
305
+ * Clear the module-level cache used by the `implementation-returns-result`
306
+ * rule to remember which exported names on a target file carry a `Result<...>`
307
+ * return annotation.
308
+ *
309
+ * Call this between runs in long-lived processes where the set of Trails
310
+ * source files may have changed on disk since the last check.
311
+ */
312
+ export const clearImplementationReturnsResultCache = (): void => {
313
+ targetFileResultExportCache.clear();
314
+ };
315
+
316
+ interface ImportBinding {
317
+ /** Local alias used in the importing file. */
318
+ readonly localName: string;
319
+ /** Original exported name from the target module. */
320
+ readonly importedName: string;
321
+ /** Raw import source specifier (e.g. './foo.js'). */
322
+ readonly source: string;
323
+ }
324
+
325
+ const getImportSourceValue = (node: AstNode): string | null => {
326
+ const sourceNode = (node as unknown as { source?: AstNode }).source;
327
+ const sourceValue = sourceNode
328
+ ? (sourceNode as unknown as { value?: unknown }).value
329
+ : undefined;
330
+ return typeof sourceValue === 'string' ? sourceValue : null;
331
+ };
332
+
333
+ const extractIdentifierName = (node: AstNode | undefined): string | null =>
334
+ node?.type === 'Identifier'
335
+ ? ((node as unknown as { name: string }).name ?? null)
336
+ : null;
337
+
338
+ const buildDefaultImportBinding = (
339
+ specifier: AstNode,
340
+ source: string
341
+ ): ImportBinding | null => {
342
+ const { local } = specifier as unknown as { local?: AstNode };
343
+ const localName = extractIdentifierName(local);
344
+ if (!localName) {
345
+ return null;
346
+ }
347
+ return { importedName: 'default', localName, source };
348
+ };
349
+
350
+ const buildNamedImportBinding = (
351
+ specifier: AstNode,
352
+ source: string
353
+ ): ImportBinding | null => {
354
+ const { local, imported } = specifier as unknown as {
355
+ local?: AstNode;
356
+ imported?: AstNode;
357
+ };
358
+ const localName = extractIdentifierName(local);
359
+ const importedName = extractIdentifierName(imported) ?? localName;
360
+ if (!(localName && importedName)) {
361
+ return null;
362
+ }
363
+ return { importedName, localName, source };
364
+ };
365
+
366
+ /**
367
+ * @remarks
368
+ * `import foo from './bar.js'` is treated as a re-export of `default` so the
369
+ * target file's `export default` declaration is considered as a potential
370
+ * Result helper. `import * as ns from './bar.js'` is handled separately by
371
+ * `collectNamespaceHelperImports`, which maps the namespace binding to the
372
+ * target's exported Result-helper names so `ns.helper(...)` member calls are
373
+ * recognized.
374
+ */
375
+ const buildImportBinding = (
376
+ specifier: AstNode,
377
+ source: string
378
+ ): ImportBinding | null => {
379
+ if (specifier.type === 'ImportDefaultSpecifier') {
380
+ return buildDefaultImportBinding(specifier, source);
381
+ }
382
+ if (specifier.type === 'ImportSpecifier') {
383
+ return buildNamedImportBinding(specifier, source);
384
+ }
385
+ return null;
386
+ };
387
+
388
+ const collectBindingsFromImportDeclaration = (
389
+ node: AstNode
390
+ ): readonly ImportBinding[] => {
391
+ const source = getImportSourceValue(node);
392
+ if (!source) {
393
+ return [];
394
+ }
395
+ const specifiers =
396
+ (node['specifiers'] as readonly AstNode[] | undefined) ?? [];
397
+ return specifiers.flatMap((specifier) => {
398
+ const binding = buildImportBinding(specifier, source);
399
+ return binding ? [binding] : [];
400
+ });
401
+ };
402
+
403
+ /** Collect `import { foo as bar } from './...'` bindings keyed by local name. */
404
+ const collectResolvableImports = (ast: AstNode): readonly ImportBinding[] => {
405
+ const imports: ImportBinding[] = [];
406
+ walk(ast, (node) => {
407
+ if (node.type === 'ImportDeclaration') {
408
+ imports.push(...collectBindingsFromImportDeclaration(node));
409
+ }
410
+ });
411
+ return imports;
412
+ };
413
+
414
+ /**
415
+ * Resolve a relative import source specifier to an absolute on-disk file path,
416
+ * or null when the source is not a relative path we can resolve locally.
417
+ *
418
+ * Handles `.js` -> `.ts` rewriting (the convention in this repo), plain `.ts`
419
+ * imports, and extensionless paths.
420
+ */
421
+ const buildResolutionCandidates = (resolved: string): readonly string[] => {
422
+ if (resolved.endsWith('.ts') || resolved.endsWith('.tsx')) {
423
+ return [resolved];
424
+ }
425
+ if (resolved.endsWith('.js')) {
426
+ return [
427
+ resolved.replace(/\.js$/, '.ts'),
428
+ resolved.replace(/\.js$/, '.tsx'),
429
+ resolved,
430
+ ];
431
+ }
432
+ if (resolved.endsWith('.jsx')) {
433
+ return [resolved.replace(/\.jsx$/, '.tsx'), resolved];
434
+ }
435
+ return [`${resolved}.ts`, `${resolved}.tsx`];
436
+ };
437
+
438
+ const resolveRelativeImportPath = (
439
+ source: string,
440
+ fromFile: string
441
+ ): string | null => {
442
+ if (!(source.startsWith('./') || source.startsWith('../'))) {
443
+ return null;
444
+ }
445
+ const baseDir = isAbsolute(fromFile)
446
+ ? dirname(fromFile)
447
+ : dirname(resolve(fromFile));
448
+ const resolved = resolve(baseDir, source);
449
+ return (
450
+ buildResolutionCandidates(resolved).find((candidate) =>
451
+ existsSync(candidate)
452
+ ) ?? null
453
+ );
454
+ };
455
+
456
+ /** Extract the declaration wrapped by an ExportNamedDeclaration, if any. */
457
+ const getExportedDeclaration = (node: AstNode): AstNode | null => {
458
+ if (node.type !== 'ExportNamedDeclaration') {
459
+ return null;
460
+ }
461
+ const decl = (node as unknown as { declaration?: AstNode }).declaration;
462
+ return decl ?? null;
463
+ };
464
+
465
+ const addExportedVariableResultHelper = (
466
+ decl: AstNode,
467
+ source: string,
468
+ collected: Set<string>
469
+ ): void => {
470
+ const declarations =
471
+ (decl['declarations'] as readonly AstNode[] | undefined) ?? [];
472
+ for (const declarator of declarations) {
473
+ const { id, init } = declarator as unknown as {
474
+ id?: AstNode;
475
+ init?: AstNode;
476
+ };
477
+ const name = extractIdentifierName(id);
478
+ if (
479
+ name &&
480
+ init &&
481
+ isFunctionLikeExpression(init) &&
482
+ hasResultReturnType(init, source)
483
+ ) {
484
+ collected.add(name);
485
+ }
486
+ }
487
+ };
488
+
489
+ const addExportedFunctionResultHelper = (
490
+ decl: AstNode,
491
+ source: string,
492
+ collected: Set<string>
493
+ ): void => {
494
+ const name = extractIdentifierName((decl as unknown as { id?: AstNode }).id);
495
+ if (name && hasResultReturnType(decl, source)) {
496
+ collected.add(name);
497
+ }
498
+ };
499
+
500
+ // ---------------------------------------------------------------------------
501
+ // Same-file declaration index (for specifier re-exports without a source)
502
+ // ---------------------------------------------------------------------------
503
+
504
+ /**
505
+ * Index a file's top-level function-like declarations (both exported-inline
506
+ * and plain) by name to the declaration node, so we can look up the original
507
+ * binding referenced by a specifier re-export like `export { helper }`.
508
+ *
509
+ * Each entry carries the init/declaration node so the caller can check the
510
+ * return-type annotation without re-walking.
511
+ */
512
+ type DeclarationIndex = ReadonlyMap<string, AstNode>;
513
+
514
+ const indexVariableDeclarationInto = (
515
+ decl: AstNode,
516
+ index: Map<string, AstNode>
517
+ ): void => {
518
+ const declarators =
519
+ (decl['declarations'] as readonly AstNode[] | undefined) ?? [];
520
+ for (const declarator of declarators) {
521
+ const { id, init } = declarator as unknown as {
522
+ id?: AstNode;
523
+ init?: AstNode;
524
+ };
525
+ const name = extractIdentifierName(id);
526
+ if (name && init && isFunctionLikeExpression(init)) {
527
+ index.set(name, init);
528
+ }
529
+ }
530
+ };
531
+
532
+ const indexFunctionDeclarationInto = (
533
+ decl: AstNode,
534
+ index: Map<string, AstNode>
535
+ ): void => {
536
+ const name = extractIdentifierName((decl as unknown as { id?: AstNode }).id);
537
+ if (name) {
538
+ index.set(name, decl);
539
+ }
540
+ };
541
+
542
+ const indexDeclarationInto = (
543
+ decl: AstNode | null | undefined,
544
+ index: Map<string, AstNode>
545
+ ): void => {
546
+ if (!decl) {
547
+ return;
548
+ }
549
+ if (decl.type === 'VariableDeclaration') {
550
+ indexVariableDeclarationInto(decl, index);
551
+ } else if (decl.type === 'FunctionDeclaration') {
552
+ indexFunctionDeclarationInto(decl, index);
553
+ }
554
+ };
555
+
556
+ const indexBodyNodeInto = (
557
+ node: AstNode,
558
+ index: Map<string, AstNode>
559
+ ): void => {
560
+ if (node.type === 'ExportNamedDeclaration') {
561
+ indexDeclarationInto(getExportedDeclaration(node), index);
562
+ return;
563
+ }
564
+ indexDeclarationInto(node, index);
565
+ };
566
+
567
+ const indexLocalDeclarations = (ast: AstNode): DeclarationIndex => {
568
+ const index = new Map<string, AstNode>();
569
+ const program = ast as unknown as { body?: readonly AstNode[] };
570
+ const bodyNodes = program.body ?? [];
571
+ for (const node of bodyNodes) {
572
+ indexBodyNodeInto(node, index);
573
+ }
574
+ return index;
575
+ };
576
+
577
+ // ---------------------------------------------------------------------------
578
+ // Export-specifier handling
579
+ // ---------------------------------------------------------------------------
580
+
581
+ interface ExportSpecifierInfo {
582
+ /** Name this export is exposed as to consumers (after `as` alias). */
583
+ readonly exportedName: string;
584
+ /** Name referenced inside the re-export (`helper` in `export { helper }`). */
585
+ readonly localName: string;
586
+ /** True when the specifier is `default` (i.e. `export { default as X }`). */
587
+ readonly isDefault: boolean;
588
+ }
589
+
590
+ const getSpecifierNameNode = (
591
+ spec: AstNode,
592
+ key: 'exported' | 'local'
593
+ ): string | null => {
594
+ const node = (spec as unknown as Record<string, AstNode | undefined>)[key];
595
+ if (!node) {
596
+ return null;
597
+ }
598
+ if (node.type === 'Identifier') {
599
+ return (node as unknown as { name?: string }).name ?? null;
600
+ }
601
+ // Support string-literal specifiers (`export { "default" as X }`, etc).
602
+ const { value } = node as unknown as { value?: unknown };
603
+ return typeof value === 'string' ? value : null;
604
+ };
605
+
606
+ const buildExportSpecifierInfo = (
607
+ spec: AstNode
608
+ ): ExportSpecifierInfo | null => {
609
+ if (spec.type !== 'ExportSpecifier') {
610
+ return null;
611
+ }
612
+ const localName = getSpecifierNameNode(spec, 'local');
613
+ const exportedName = getSpecifierNameNode(spec, 'exported') ?? localName;
614
+ if (!(localName && exportedName)) {
615
+ return null;
616
+ }
617
+ return {
618
+ exportedName,
619
+ isDefault: localName === 'default',
620
+ localName,
621
+ };
622
+ };
623
+
624
+ const getExportDefaultDeclaration = (ast: AstNode): AstNode | null => {
625
+ const program = ast as unknown as { body?: readonly AstNode[] };
626
+ const bodyNodes = program.body ?? [];
627
+ for (const node of bodyNodes) {
628
+ if (node.type === 'ExportDefaultDeclaration') {
629
+ const decl = (node as unknown as { declaration?: AstNode }).declaration;
630
+ return decl ?? null;
631
+ }
632
+ }
633
+ return null;
634
+ };
635
+
636
+ // Bounded recursion: one transitive hop through `export { ... } from`.
637
+ const MAX_RERESOLVE_DEPTH = 1;
638
+
639
+ /** Check whether a local declaration node has a `Result<...>` return annotation. */
640
+ const isResultHelperDeclaration = (
641
+ declarationNode: AstNode | undefined,
642
+ source: string
643
+ ): boolean => {
644
+ if (!declarationNode) {
645
+ return false;
646
+ }
647
+ if (isFunctionLikeExpression(declarationNode)) {
648
+ return hasResultReturnType(declarationNode, source);
649
+ }
650
+ if (declarationNode.type === 'FunctionDeclaration') {
651
+ return hasResultReturnType(declarationNode, source);
652
+ }
653
+ return false;
654
+ };
655
+
656
+ /** Resolve an `export default ...` declaration, following one identifier hop. */
657
+ const checkDefaultDeclarationIsResultHelper = (
658
+ defaultDecl: AstNode,
659
+ targetSource: string,
660
+ targetLocalDeclarations: DeclarationIndex
661
+ ): boolean => {
662
+ if (isResultHelperDeclaration(defaultDecl, targetSource)) {
663
+ return true;
664
+ }
665
+ if (defaultDecl.type === 'Identifier') {
666
+ const name = extractIdentifierName(defaultDecl);
667
+ const referenced = name ? targetLocalDeclarations.get(name) : undefined;
668
+ return isResultHelperDeclaration(referenced, targetSource);
669
+ }
670
+ return false;
671
+ };
672
+
673
+ interface LoadedTargetFile {
674
+ readonly ast: AstNode;
675
+ readonly source: string;
676
+ readonly localDeclarations: DeclarationIndex;
677
+ }
678
+
679
+ const loadTargetFile = (targetPath: string): LoadedTargetFile | null => {
680
+ try {
681
+ const source = readFileSync(targetPath, 'utf8');
682
+ const ast = parse(targetPath, source) as AstNode | null;
683
+ if (!ast) {
684
+ return null;
685
+ }
686
+ return {
687
+ ast,
688
+ localDeclarations: indexLocalDeclarations(ast),
689
+ source,
690
+ };
691
+ } catch {
692
+ return null;
693
+ }
694
+ };
695
+
696
+ interface ReExportContext {
697
+ readonly loadedTarget: LoadedTargetFile | null;
698
+ readonly downstreamResultNames: ReadonlySet<string>;
699
+ }
700
+
701
+ const applyDefaultSpecifier = (
702
+ info: ExportSpecifierInfo,
703
+ loadedTarget: LoadedTargetFile | null,
704
+ collected: Set<string>
705
+ ): void => {
706
+ if (!loadedTarget) {
707
+ return;
708
+ }
709
+ const defaultDecl = getExportDefaultDeclaration(loadedTarget.ast);
710
+ if (!defaultDecl) {
711
+ return;
712
+ }
713
+ if (
714
+ checkDefaultDeclarationIsResultHelper(
715
+ defaultDecl,
716
+ loadedTarget.source,
717
+ loadedTarget.localDeclarations
718
+ )
719
+ ) {
720
+ collected.add(info.exportedName);
721
+ }
722
+ };
723
+
724
+ const applySpecifierInfo = (
725
+ info: ExportSpecifierInfo,
726
+ ctx: ReExportContext,
727
+ collected: Set<string>
728
+ ): void => {
729
+ if (info.isDefault) {
730
+ applyDefaultSpecifier(info, ctx.loadedTarget, collected);
731
+ return;
732
+ }
733
+ if (ctx.downstreamResultNames.has(info.localName)) {
734
+ collected.add(info.exportedName);
735
+ }
736
+ };
737
+
738
+ const resolveReExportTargetPath = (
739
+ node: AstNode,
740
+ targetPath: string,
741
+ visited: ReadonlySet<string>,
742
+ depth: number
743
+ ): string | null => {
744
+ if (depth >= MAX_RERESOLVE_DEPTH) {
745
+ return null;
746
+ }
747
+ const reSource = getImportSourceValue(node);
748
+ if (!reSource) {
749
+ return null;
750
+ }
751
+ const reTargetPath = resolveRelativeImportPath(reSource, targetPath);
752
+ if (!reTargetPath || visited.has(reTargetPath)) {
753
+ return null;
754
+ }
755
+ return reTargetPath;
756
+ };
757
+
758
+ const buildReExportContext = (
759
+ reTargetPath: string,
760
+ specifierInfos: readonly ExportSpecifierInfo[],
761
+ targetPath: string,
762
+ visited: ReadonlySet<string>,
763
+ depth: number
764
+ ): ReExportContext => {
765
+ const needsDefault = specifierInfos.some((info) => info.isDefault);
766
+ // Load once when the default specifier branch needs the target AST; the
767
+ // same loaded object is threaded into the downstream walk so it isn't
768
+ // read and parsed a second time within this check() call.
769
+ const loadedTarget = needsDefault ? loadTargetFile(reTargetPath) : null;
770
+ // eslint-disable-next-line no-use-before-define
771
+ const downstreamResultNames = collectTargetExportedResultHelperNames(
772
+ reTargetPath,
773
+ visited,
774
+ targetPath,
775
+ depth + 1,
776
+ loadedTarget
777
+ );
778
+ return {
779
+ downstreamResultNames,
780
+ loadedTarget,
781
+ };
782
+ };
783
+
784
+ /**
785
+ * Resolve a re-export with source (`export { ... } from './x.js'`) by pulling
786
+ * the matching names off the target file, honoring aliases and `default`.
787
+ */
788
+ const resolveReExportWithSource = (
789
+ node: AstNode,
790
+ specifiers: readonly AstNode[],
791
+ targetPath: string,
792
+ visited: ReadonlySet<string>,
793
+ depth: number,
794
+ collected: Set<string>
795
+ ): void => {
796
+ const reTargetPath = resolveReExportTargetPath(
797
+ node,
798
+ targetPath,
799
+ visited,
800
+ depth
801
+ );
802
+ if (!reTargetPath) {
803
+ return;
804
+ }
805
+ const specifierInfos = specifiers.flatMap((spec) => {
806
+ const info = buildExportSpecifierInfo(spec);
807
+ return info ? [info] : [];
808
+ });
809
+ const ctx = buildReExportContext(
810
+ reTargetPath,
811
+ specifierInfos,
812
+ targetPath,
813
+ visited,
814
+ depth
815
+ );
816
+ for (const info of specifierInfos) {
817
+ applySpecifierInfo(info, ctx, collected);
818
+ }
819
+ };
820
+
821
+ /** Resolve a specifier-only re-export (`export { helper };`) against same-file declarations. */
822
+ const resolveReExportWithoutSource = (
823
+ specifiers: readonly AstNode[],
824
+ localDeclarations: DeclarationIndex,
825
+ source: string,
826
+ collected: Set<string>
827
+ ): void => {
828
+ for (const spec of specifiers) {
829
+ const info = buildExportSpecifierInfo(spec);
830
+ if (!info || info.isDefault) {
831
+ continue;
832
+ }
833
+ if (
834
+ isResultHelperDeclaration(localDeclarations.get(info.localName), source)
835
+ ) {
836
+ collected.add(info.exportedName);
837
+ }
838
+ }
839
+ };
840
+
841
+ const processInlineExportedDeclaration = (
842
+ exportedDecl: AstNode,
843
+ source: string,
844
+ collected: Set<string>
845
+ ): boolean => {
846
+ if (exportedDecl.type === 'VariableDeclaration') {
847
+ addExportedVariableResultHelper(exportedDecl, source, collected);
848
+ return true;
849
+ }
850
+ if (exportedDecl.type === 'FunctionDeclaration') {
851
+ addExportedFunctionResultHelper(exportedDecl, source, collected);
852
+ return true;
853
+ }
854
+ return false;
855
+ };
856
+
857
+ const processExportNamedDeclaration = (
858
+ node: AstNode,
859
+ source: string,
860
+ targetPath: string,
861
+ visited: ReadonlySet<string>,
862
+ depth: number,
863
+ localDeclarations: DeclarationIndex,
864
+ collected: Set<string>
865
+ ): void => {
866
+ const exportedDecl = getExportedDeclaration(node);
867
+ if (
868
+ exportedDecl &&
869
+ processInlineExportedDeclaration(exportedDecl, source, collected)
870
+ ) {
871
+ return;
872
+ }
873
+ const specifiers =
874
+ (node['specifiers'] as readonly AstNode[] | undefined) ?? [];
875
+ if (specifiers.length === 0) {
876
+ return;
877
+ }
878
+ if (getImportSourceValue(node)) {
879
+ resolveReExportWithSource(
880
+ node,
881
+ specifiers,
882
+ targetPath,
883
+ visited,
884
+ depth,
885
+ collected
886
+ );
887
+ return;
888
+ }
889
+ resolveReExportWithoutSource(
890
+ specifiers,
891
+ localDeclarations,
892
+ source,
893
+ collected
894
+ );
895
+ };
896
+
897
+ const processExportDefaultDeclaration = (
898
+ node: AstNode,
899
+ source: string,
900
+ localDeclarations: DeclarationIndex,
901
+ collected: Set<string>
902
+ ): void => {
903
+ const defaultDecl = (node as unknown as { declaration?: AstNode })
904
+ .declaration;
905
+ if (!defaultDecl) {
906
+ return;
907
+ }
908
+ if (
909
+ checkDefaultDeclarationIsResultHelper(
910
+ defaultDecl,
911
+ source,
912
+ localDeclarations
913
+ )
914
+ ) {
915
+ collected.add('default');
916
+ }
917
+ };
918
+
919
+ const collectExportedResultHelpersFromAst = (
920
+ ast: AstNode,
921
+ source: string,
922
+ targetPath: string,
923
+ visited: ReadonlySet<string>,
924
+ depth: number,
925
+ preloadedLocalDeclarations: DeclarationIndex | null = null
926
+ ): ReadonlySet<string> => {
927
+ const collected = new Set<string>();
928
+ // Reuse the preloaded declaration index when available (e.g., threaded in
929
+ // from `loadTargetFile`) to avoid re-walking the same AST.
930
+ const localDeclarations =
931
+ preloadedLocalDeclarations ?? indexLocalDeclarations(ast);
932
+ const program = ast as unknown as { body?: readonly AstNode[] };
933
+ const bodyNodes = program.body ?? [];
934
+
935
+ for (const node of bodyNodes) {
936
+ if (node.type === 'ExportNamedDeclaration') {
937
+ processExportNamedDeclaration(
938
+ node,
939
+ source,
940
+ targetPath,
941
+ visited,
942
+ depth,
943
+ localDeclarations,
944
+ collected
945
+ );
946
+ } else if (node.type === 'ExportDefaultDeclaration') {
947
+ processExportDefaultDeclaration(
948
+ node,
949
+ source,
950
+ localDeclarations,
951
+ collected
952
+ );
953
+ } else if (node.type === 'ExportAllDeclaration') {
954
+ // eslint-disable-next-line no-use-before-define
955
+ processExportAllDeclaration(node, targetPath, visited, depth, collected);
956
+ }
957
+ }
958
+
959
+ return collected;
960
+ };
961
+
962
+ /**
963
+ * Handle `export * from './x.js'` by recursing into the target module and
964
+ * unioning its exported Result-helper names. Type-only re-exports
965
+ * (`export type * from '...'`) contribute nothing. Bounded by
966
+ * `MAX_RERESOLVE_DEPTH` and the visited-set cycle guard shared with the
967
+ * specifier re-export path.
968
+ */
969
+ const processExportAllDeclaration = (
970
+ node: AstNode,
971
+ targetPath: string,
972
+ visited: ReadonlySet<string>,
973
+ depth: number,
974
+ collected: Set<string>
975
+ ): void => {
976
+ const { exportKind } = node as unknown as { exportKind?: string };
977
+ if (exportKind === 'type') {
978
+ return;
979
+ }
980
+ const reTargetPath = resolveReExportTargetPath(
981
+ node,
982
+ targetPath,
983
+ visited,
984
+ depth
985
+ );
986
+ if (!reTargetPath) {
987
+ return;
988
+ }
989
+ // eslint-disable-next-line no-use-before-define
990
+ const downstream = collectTargetExportedResultHelperNames(
991
+ reTargetPath,
992
+ visited,
993
+ targetPath,
994
+ depth + 1
995
+ );
996
+ // `export * from` does NOT re-export the default binding, so we union
997
+ // only the named Result helpers from the downstream module.
998
+ for (const name of downstream) {
999
+ if (name !== 'default') {
1000
+ collected.add(name);
1001
+ }
1002
+ }
1003
+ };
1004
+
1005
+ const parseTargetResultHelperNames = (
1006
+ targetPath: string,
1007
+ visited: ReadonlySet<string>,
1008
+ depth: number,
1009
+ preloaded: LoadedTargetFile | null = null
1010
+ ): ReadonlySet<string> => {
1011
+ const loaded = preloaded ?? loadTargetFile(targetPath);
1012
+ if (!loaded) {
1013
+ return new Set<string>();
1014
+ }
1015
+ return collectExportedResultHelpersFromAst(
1016
+ loaded.ast,
1017
+ loaded.source,
1018
+ targetPath,
1019
+ visited,
1020
+ depth,
1021
+ loaded.localDeclarations
1022
+ );
1023
+ };
1024
+
1025
+ const buildVisitedPathSet = (
1026
+ parentVisited: ReadonlySet<string>,
1027
+ targetPath: string,
1028
+ parentPath: string | undefined
1029
+ ): ReadonlySet<string> => {
1030
+ const seeds = [...parentVisited, targetPath];
1031
+ if (parentPath) {
1032
+ seeds.push(parentPath);
1033
+ }
1034
+ return new Set<string>(seeds);
1035
+ };
1036
+
1037
+ /**
1038
+ * Collect the set of exported names from a target file whose declaration has
1039
+ * an explicit `Result<...>` / `Promise<Result<...>>` return annotation.
1040
+ *
1041
+ * Uses a visited-set on the recursion path to guard against `export { ... }
1042
+ * from` import cycles between files. Depth is capped at a single transitive
1043
+ * hop (see `MAX_RERESOLVE_DEPTH`) — deeper chains silently fall back.
1044
+ */
1045
+ // Only the direct-import path (no parents visited) is safe to cache: the
1046
+ // computed set is a function of (targetPath, parentVisited), and
1047
+ // cycle-truncated results from transitive walks must not bleed into later
1048
+ // direct lookups. See PR #204 review.
1049
+ const readCachedResultExports = (
1050
+ targetPath: string,
1051
+ parentVisited: ReadonlySet<string>
1052
+ ): ReadonlySet<string> | undefined => {
1053
+ if (parentVisited.size !== 0) {
1054
+ return;
1055
+ }
1056
+ return targetFileResultExportCache.get(targetPath);
1057
+ };
1058
+
1059
+ // biome-ignore lint/style/useConst: declared as a function so hoisting lets `buildReExportContext` (a const declared earlier) reference it before its textual definition
1060
+ // eslint-disable-next-line func-style, no-use-before-define
1061
+ function collectTargetExportedResultHelperNames(
1062
+ targetPath: string,
1063
+ parentVisited: ReadonlySet<string> = new Set<string>(),
1064
+ parentPath?: string,
1065
+ depth = 0,
1066
+ preloaded: LoadedTargetFile | null = null
1067
+ ): ReadonlySet<string> {
1068
+ if (parentVisited.has(targetPath)) {
1069
+ return new Set<string>();
1070
+ }
1071
+ const cached = readCachedResultExports(targetPath, parentVisited);
1072
+ if (cached) {
1073
+ return cached;
1074
+ }
1075
+ const visited = buildVisitedPathSet(parentVisited, targetPath, parentPath);
1076
+ const names = parseTargetResultHelperNames(
1077
+ targetPath,
1078
+ visited,
1079
+ depth,
1080
+ preloaded
1081
+ );
1082
+ if (parentVisited.size === 0) {
1083
+ targetFileResultExportCache.set(targetPath, names);
1084
+ }
1085
+ return names;
1086
+ }
1087
+
1088
+ /**
1089
+ * Extend a local-helper-name set with Result-returning helpers imported from
1090
+ * relative modules. Falls back silently on any resolution/parse failure.
1091
+ */
1092
+ const collectImportedResultHelperNames = (
1093
+ ast: AstNode,
1094
+ filePath: string
1095
+ ): ReadonlySet<string> => {
1096
+ const names = new Set<string>();
1097
+
1098
+ for (const binding of collectResolvableImports(ast)) {
1099
+ const targetPath = resolveRelativeImportPath(binding.source, filePath);
1100
+ if (!targetPath) {
1101
+ continue;
1102
+ }
1103
+ const exportedResultNames =
1104
+ collectTargetExportedResultHelperNames(targetPath);
1105
+ if (exportedResultNames.has(binding.importedName)) {
1106
+ names.add(binding.localName);
1107
+ }
1108
+ }
1109
+
1110
+ return names;
1111
+ };
1112
+
1113
+ interface NamespaceEntry {
1114
+ readonly localName: string;
1115
+ readonly names: ReadonlySet<string>;
1116
+ }
1117
+
1118
+ /** Extract a namespace specifier's local name if it is a namespace import. */
1119
+ const getNamespaceLocalName = (spec: AstNode): string | null => {
1120
+ if (spec.type !== 'ImportNamespaceSpecifier') {
1121
+ return null;
1122
+ }
1123
+ const { local } = spec as unknown as { local?: AstNode };
1124
+ return extractIdentifierName(local);
1125
+ };
1126
+
1127
+ /**
1128
+ * Resolve a single namespace specifier to (localName, resultHelperNames), or
1129
+ * null when the specifier is not a resolvable namespace import.
1130
+ *
1131
+ * We intentionally record the namespace even when the target file exports no
1132
+ * Result helpers (empty set). `isNamespaceHelperMemberCall` can then identify
1133
+ * `ns.anything()` as a namespace member call against a non-Result-helper
1134
+ * target — which correctly falls through to the general return-value
1135
+ * diagnostic path. Dropping the entry would misclassify the call as a
1136
+ * *non-namespace* member call and skip the namespace-shadowing scope check.
1137
+ */
1138
+ const resolveNamespaceSpecifier = (
1139
+ spec: AstNode,
1140
+ source: string,
1141
+ filePath: string
1142
+ ): NamespaceEntry | null => {
1143
+ const localName = getNamespaceLocalName(spec);
1144
+ if (!localName) {
1145
+ return null;
1146
+ }
1147
+ const targetPath = resolveRelativeImportPath(source, filePath);
1148
+ if (!targetPath) {
1149
+ return null;
1150
+ }
1151
+ const names = collectTargetExportedResultHelperNames(targetPath);
1152
+ return { localName, names };
1153
+ };
1154
+
1155
+ /** Extract namespace helper entries from a single ImportDeclaration node. */
1156
+ const namespaceEntriesFromImport = (
1157
+ node: AstNode,
1158
+ filePath: string
1159
+ ): readonly NamespaceEntry[] => {
1160
+ const source = getImportSourceValue(node);
1161
+ if (!source) {
1162
+ return [];
1163
+ }
1164
+ const specifiers =
1165
+ (node['specifiers'] as readonly AstNode[] | undefined) ?? [];
1166
+ return specifiers.flatMap((spec) => {
1167
+ const entry = resolveNamespaceSpecifier(spec, source, filePath);
1168
+ return entry ? [entry] : [];
1169
+ });
1170
+ };
1171
+
1172
+ /**
1173
+ * Collect `import * as ns from './foo.js'` bindings and map each local
1174
+ * namespace name to the set of Result-returning helper names exported by the
1175
+ * resolved target module. Returns an empty map if no namespace imports are
1176
+ * found or none resolve to local files.
1177
+ */
1178
+ const collectNamespaceHelperImports = (
1179
+ ast: AstNode,
1180
+ filePath: string
1181
+ ): NamespaceHelperMap => {
1182
+ const map = new Map<string, ReadonlySet<string>>();
1183
+ walk(ast, (node) => {
1184
+ if (node.type !== 'ImportDeclaration') {
1185
+ return;
1186
+ }
1187
+ for (const { localName, names } of namespaceEntriesFromImport(
1188
+ node,
1189
+ filePath
1190
+ )) {
1191
+ map.set(localName, names);
1192
+ }
1193
+ });
1194
+ return map;
1195
+ };
1196
+
1197
+ /**
1198
+ * Combine same-file helper names with helpers imported from relative modules.
1199
+ */
1200
+ const collectAllResultHelperNames = (
1201
+ ast: AstNode,
1202
+ sourceCode: string,
1203
+ filePath: string
1204
+ ): ReadonlySet<string> => {
1205
+ const local = collectResultHelperNames(ast, sourceCode);
1206
+ const imported = collectImportedResultHelperNames(ast, filePath);
1207
+ if (imported.size === 0) {
1208
+ return local;
1209
+ }
1210
+ const merged = new Set<string>(local);
1211
+ for (const name of imported) {
1212
+ merged.add(name);
1213
+ }
1214
+ return merged;
1215
+ };
1216
+
311
1217
  // ---------------------------------------------------------------------------
312
1218
  // Per-implementation checking
313
1219
  // ---------------------------------------------------------------------------
@@ -318,6 +1224,7 @@ const checkImplementation = (
318
1224
  filePath: string,
319
1225
  sourceCode: string,
320
1226
  helperNames: ReadonlySet<string>,
1227
+ namespaceHelpers: NamespaceHelperMap,
321
1228
  diagnostics: WardenDiagnostic[]
322
1229
  ): void => {
323
1230
  const fnBody = (implValue as unknown as { body?: AstNode }).body;
@@ -325,6 +1232,10 @@ const checkImplementation = (
325
1232
  return;
326
1233
  }
327
1234
 
1235
+ // Seed analysis with the implementation's own bindings so parameter names
1236
+ // and hoisted vars shadow namespace imports in both block and concise bodies.
1237
+ const implScope = collectScopeFrameBindings(implValue);
1238
+
328
1239
  if (fnBody.type === 'BlockStatement' || fnBody.type === 'FunctionBody') {
329
1240
  checkReturnStatements(
330
1241
  fnBody,
@@ -332,12 +1243,19 @@ const checkImplementation = (
332
1243
  filePath,
333
1244
  sourceCode,
334
1245
  helperNames,
335
- diagnostics
1246
+ namespaceHelpers,
1247
+ diagnostics,
1248
+ implScope
336
1249
  );
337
1250
  return;
338
1251
  }
339
1252
 
340
- if (!isResultExpression(fnBody) && !isHelperCall(fnBody, helperNames)) {
1253
+ const conciseScopes: readonly ReadonlySet<string>[] =
1254
+ implScope.size > 0 ? [implScope] : [];
1255
+ if (
1256
+ !isResultExpression(fnBody) &&
1257
+ !isHelperCall(fnBody, helperNames, namespaceHelpers, conciseScopes)
1258
+ ) {
341
1259
  diagnostics.push({
342
1260
  filePath,
343
1261
  line: offsetToLine(sourceCode, implValue.start),
@@ -358,7 +1276,8 @@ const checkAllDefinitions = (
358
1276
  sourceCode: string
359
1277
  ): WardenDiagnostic[] => {
360
1278
  const diagnostics: WardenDiagnostic[] = [];
361
- const helperNames = collectResultHelperNames(ast, sourceCode);
1279
+ const helperNames = collectAllResultHelperNames(ast, sourceCode, filePath);
1280
+ const namespaceHelpers = collectNamespaceHelperImports(ast, filePath);
362
1281
 
363
1282
  for (const def of findTrailDefinitions(ast)) {
364
1283
  const info = { id: def.id, label: 'Trail' };
@@ -369,6 +1288,7 @@ const checkAllDefinitions = (
369
1288
  filePath,
370
1289
  sourceCode,
371
1290
  helperNames,
1291
+ namespaceHelpers,
372
1292
  diagnostics
373
1293
  );
374
1294
  }