@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
@@ -1,9 +1,45 @@
1
- import { describe, expect, test } from 'bun:test';
1
+ import { afterAll, beforeAll, describe, expect, spyOn, test } from 'bun:test';
2
+ import * as nodeFs from 'node:fs';
3
+ import { mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
4
+ import { tmpdir } from 'node:os';
5
+ import { join } from 'node:path';
2
6
 
3
- import { implementationReturnsResult } from '../rules/implementation-returns-result.js';
7
+ import {
8
+ clearImplementationReturnsResultCache,
9
+ implementationReturnsResult,
10
+ } from '../rules/implementation-returns-result.js';
4
11
 
5
12
  const TEST_FILE = 'test.ts';
6
13
 
14
+ const writeReadCountFixture = (
15
+ writeFile: (name: string, content: string) => string
16
+ ): { readonly implPath: string; readonly caller: string } => {
17
+ const implPath = writeFile(
18
+ 'impl-readcount.ts',
19
+ `const helper = async (): Promise<Result<object, Error>> =>
20
+ Result.ok({ ok: true });
21
+
22
+ export default helper;
23
+ `
24
+ );
25
+ writeFile(
26
+ 'barrel-readcount.ts',
27
+ `export { default as foo } from './impl-readcount.js';
28
+ `
29
+ );
30
+ const caller = writeFile(
31
+ 'caller-readcount.ts',
32
+ `import { foo } from './barrel-readcount.js';
33
+
34
+ trail("entity.report", {
35
+ blaze: async (input, ctx) => {
36
+ return foo();
37
+ }
38
+ })`
39
+ );
40
+ return { caller, implPath };
41
+ };
42
+
7
43
  describe('implementation-returns-result', () => {
8
44
  test('flags raw object return in trail implementation', () => {
9
45
  const code = `
@@ -104,6 +140,984 @@ trail("entity.list", {
104
140
  expect(diagnostics[0]?.message).toContain('entity.list');
105
141
  });
106
142
 
143
+ describe('imported helpers', () => {
144
+ let tmpDir: string;
145
+
146
+ beforeAll(() => {
147
+ tmpDir = mkdtempSync(join(tmpdir(), 'warden-impl-result-'));
148
+ });
149
+
150
+ afterAll(() => {
151
+ rmSync(tmpDir, { force: true, recursive: true });
152
+ });
153
+
154
+ const writeFile = (name: string, content: string): string => {
155
+ const path = join(tmpDir, name);
156
+ writeFileSync(path, content);
157
+ return path;
158
+ };
159
+
160
+ test('allows imported helper with Promise<Result<...>> return annotation', () => {
161
+ writeFile(
162
+ 'result-helper.ts',
163
+ `export const buildReport = async (): Promise<Result<object, Error>> =>
164
+ Result.ok({ ok: true });
165
+ `
166
+ );
167
+ const caller = writeFile(
168
+ 'caller.ts',
169
+ `import { buildReport } from './result-helper.js';
170
+
171
+ trail("entity.report", {
172
+ blaze: async (input, ctx) => {
173
+ return buildReport();
174
+ }
175
+ })`
176
+ );
177
+
178
+ const diagnostics = implementationReturnsResult.check(
179
+ readFileSync(caller, 'utf8'),
180
+ caller
181
+ );
182
+
183
+ expect(diagnostics.length).toBe(0);
184
+ });
185
+
186
+ test('flags imported helper without Result return annotation', () => {
187
+ writeFile(
188
+ 'plain-helper.ts',
189
+ `export const buildReport = async () => ({ ok: true });
190
+ `
191
+ );
192
+ const caller = writeFile(
193
+ 'caller-plain.ts',
194
+ `import { buildReport } from './plain-helper.js';
195
+
196
+ trail("entity.report", {
197
+ blaze: async (input, ctx) => {
198
+ return buildReport();
199
+ }
200
+ })`
201
+ );
202
+
203
+ const diagnostics = implementationReturnsResult.check(
204
+ readFileSync(caller, 'utf8'),
205
+ caller
206
+ );
207
+
208
+ expect(diagnostics.length).toBe(1);
209
+ });
210
+
211
+ test('flags helper imported from bare specifier (node_modules)', () => {
212
+ const caller = writeFile(
213
+ 'caller-bare.ts',
214
+ `import { buildReport } from 'some-package';
215
+
216
+ trail("entity.report", {
217
+ blaze: async (input, ctx) => {
218
+ return buildReport();
219
+ }
220
+ })`
221
+ );
222
+
223
+ const diagnostics = implementationReturnsResult.check(
224
+ readFileSync(caller, 'utf8'),
225
+ caller
226
+ );
227
+
228
+ expect(diagnostics.length).toBe(1);
229
+ });
230
+
231
+ test('flags gracefully when target file is unreadable', () => {
232
+ const caller = writeFile(
233
+ 'caller-missing.ts',
234
+ `import { buildReport } from './does-not-exist.js';
235
+
236
+ trail("entity.report", {
237
+ blaze: async (input, ctx) => {
238
+ return buildReport();
239
+ }
240
+ })`
241
+ );
242
+
243
+ const diagnostics = implementationReturnsResult.check(
244
+ readFileSync(caller, 'utf8'),
245
+ caller
246
+ );
247
+
248
+ expect(diagnostics.length).toBe(1);
249
+ });
250
+
251
+ describe('specifier re-exports', () => {
252
+ test('allows specifier re-export with source (export { helper } from ...)', () => {
253
+ writeFile(
254
+ 'impl-specifier.ts',
255
+ `export const helper = async (): Promise<Result<object, Error>> =>
256
+ Result.ok({ ok: true });
257
+ `
258
+ );
259
+ writeFile(
260
+ 'barrel-specifier.ts',
261
+ `export { helper } from './impl-specifier.js';
262
+ `
263
+ );
264
+ const caller = writeFile(
265
+ 'caller-specifier.ts',
266
+ `import { helper } from './barrel-specifier.js';
267
+
268
+ trail("entity.report", {
269
+ blaze: async (input, ctx) => {
270
+ return helper();
271
+ }
272
+ })`
273
+ );
274
+
275
+ const diagnostics = implementationReturnsResult.check(
276
+ readFileSync(caller, 'utf8'),
277
+ caller
278
+ );
279
+
280
+ expect(diagnostics.length).toBe(0);
281
+ });
282
+
283
+ test('allows aliased specifier re-export (export { helper as aliased })', () => {
284
+ writeFile(
285
+ 'impl-aliased.ts',
286
+ `export const helper = async (): Promise<Result<object, Error>> =>
287
+ Result.ok({ ok: true });
288
+ `
289
+ );
290
+ writeFile(
291
+ 'barrel-aliased.ts',
292
+ `export { helper as aliased } from './impl-aliased.js';
293
+ `
294
+ );
295
+ const caller = writeFile(
296
+ 'caller-aliased.ts',
297
+ `import { aliased } from './barrel-aliased.js';
298
+
299
+ trail("entity.report", {
300
+ blaze: async (input, ctx) => {
301
+ return aliased();
302
+ }
303
+ })`
304
+ );
305
+
306
+ const diagnostics = implementationReturnsResult.check(
307
+ readFileSync(caller, 'utf8'),
308
+ caller
309
+ );
310
+
311
+ expect(diagnostics.length).toBe(0);
312
+ });
313
+
314
+ test('allows specifier re-export without source (same-file)', () => {
315
+ writeFile(
316
+ 'barrel-samefile.ts',
317
+ `const helper = async (): Promise<Result<object, Error>> =>
318
+ Result.ok({ ok: true });
319
+
320
+ export { helper };
321
+ `
322
+ );
323
+ const caller = writeFile(
324
+ 'caller-samefile.ts',
325
+ `import { helper } from './barrel-samefile.js';
326
+
327
+ trail("entity.report", {
328
+ blaze: async (input, ctx) => {
329
+ return helper();
330
+ }
331
+ })`
332
+ );
333
+
334
+ const diagnostics = implementationReturnsResult.check(
335
+ readFileSync(caller, 'utf8'),
336
+ caller
337
+ );
338
+
339
+ expect(diagnostics.length).toBe(0);
340
+ });
341
+
342
+ test('allows default re-export (export { default as foo } from ...)', () => {
343
+ writeFile(
344
+ 'impl-default.ts',
345
+ `const helper = async (): Promise<Result<object, Error>> =>
346
+ Result.ok({ ok: true });
347
+
348
+ export default helper;
349
+ `
350
+ );
351
+ writeFile(
352
+ 'barrel-default.ts',
353
+ `export { default as foo } from './impl-default.js';
354
+ `
355
+ );
356
+ const caller = writeFile(
357
+ 'caller-default.ts',
358
+ `import { foo } from './barrel-default.js';
359
+
360
+ trail("entity.report", {
361
+ blaze: async (input, ctx) => {
362
+ return foo();
363
+ }
364
+ })`
365
+ );
366
+
367
+ const diagnostics = implementationReturnsResult.check(
368
+ readFileSync(caller, 'utf8'),
369
+ caller
370
+ );
371
+
372
+ expect(diagnostics.length).toBe(0);
373
+ });
374
+
375
+ test('reads the re-export target file only once per check (default specifier)', () => {
376
+ // Regression for PR #204: `export { default as foo } from './impl.js'`
377
+ // previously triggered two reads/parses of impl.js within a single
378
+ // check() call — once via the downstream-names walk and once for the
379
+ // default-specifier AST lookup. The loaded target is now threaded
380
+ // through, so impl.js should be read exactly once.
381
+ const { implPath, caller } = writeReadCountFixture(writeFile);
382
+
383
+ clearImplementationReturnsResultCache();
384
+ const readSpy = spyOn(nodeFs, 'readFileSync');
385
+
386
+ try {
387
+ const diagnostics = implementationReturnsResult.check(
388
+ readFileSync(caller, 'utf8'),
389
+ caller
390
+ );
391
+ expect(diagnostics.length).toBe(0);
392
+ const implReads = readSpy.mock.calls.filter(
393
+ (call) => call[0] === implPath
394
+ );
395
+ expect(implReads.length).toBe(1);
396
+ } finally {
397
+ readSpy.mockRestore();
398
+ }
399
+ });
400
+
401
+ test('caps re-export chains beyond one transitive hop', () => {
402
+ // A -> B -> C: caller imports from A, which re-exports from B,
403
+ // which re-exports from C where the helper is declared. The
404
+ // MAX_RERESOLVE_DEPTH=1 cap should prevent resolving C, so the
405
+ // helper name is NOT recognized through the 2-hop chain.
406
+ writeFile(
407
+ 'depth-c.ts',
408
+ `export const deepHelper = async (): Promise<Result<object, Error>> =>
409
+ Result.ok({ ok: true });
410
+ `
411
+ );
412
+ writeFile(
413
+ 'depth-b.ts',
414
+ `export { deepHelper } from './depth-c.js';
415
+ `
416
+ );
417
+ writeFile(
418
+ 'depth-a.ts',
419
+ `export { deepHelper } from './depth-b.js';
420
+ `
421
+ );
422
+ const caller = writeFile(
423
+ 'caller-depth.ts',
424
+ `import { deepHelper } from './depth-a.js';
425
+
426
+ trail("entity.report", {
427
+ blaze: async (input, ctx) => {
428
+ return deepHelper();
429
+ }
430
+ })`
431
+ );
432
+
433
+ const diagnostics = implementationReturnsResult.check(
434
+ readFileSync(caller, 'utf8'),
435
+ caller
436
+ );
437
+
438
+ // 2-hop re-export chain exceeds MAX_RERESOLVE_DEPTH, so the helper's
439
+ // Result annotation is not discoverable and the return is flagged.
440
+ expect(diagnostics.length).toBe(1);
441
+ });
442
+
443
+ test('cache does not bleed cycle-truncated results into direct imports', () => {
444
+ // A -> B -> A cycle. When A is resolved first (e.g. through a caller
445
+ // that imports from A), resolving B is attempted while A is in the
446
+ // visited set, which truncates B's transitive view back to A. A naive
447
+ // per-target cache would then persist an empty set for B and wrongly
448
+ // flag a later direct import from B.
449
+ writeFile(
450
+ 'ctx-a.ts',
451
+ `export { helper } from './ctx-b.js';
452
+ `
453
+ );
454
+ writeFile(
455
+ 'ctx-b.ts',
456
+ `export const helper = async (): Promise<Result<object, Error>> =>
457
+ Result.ok({ ok: true });
458
+
459
+ export { helper as aliasedFromA } from './ctx-a.js';
460
+ `
461
+ );
462
+ const callerA = writeFile(
463
+ 'caller-ctx-a.ts',
464
+ `import { helper } from './ctx-a.js';
465
+
466
+ trail("entity.first", {
467
+ blaze: async (input, ctx) => {
468
+ return helper();
469
+ }
470
+ })`
471
+ );
472
+ const callerB = writeFile(
473
+ 'caller-ctx-b.ts',
474
+ `import { helper } from './ctx-b.js';
475
+
476
+ trail("entity.second", {
477
+ blaze: async (input, ctx) => {
478
+ return helper();
479
+ }
480
+ })`
481
+ );
482
+
483
+ // Resolve A first — this walks A -> B (and B -> A is cycle-guarded).
484
+ implementationReturnsResult.check(
485
+ readFileSync(callerA, 'utf8'),
486
+ callerA
487
+ );
488
+
489
+ // Now resolve B directly. B's own inline helper must still be
490
+ // recognized as Result-returning, even though an earlier transitive
491
+ // walk touched B under a non-empty parentVisited.
492
+ const diagnostics = implementationReturnsResult.check(
493
+ readFileSync(callerB, 'utf8'),
494
+ callerB
495
+ );
496
+
497
+ expect(diagnostics.length).toBe(0);
498
+ });
499
+
500
+ test('allows default-imported Result helper (import foo from ...)', () => {
501
+ writeFile(
502
+ 'impl-default-import.ts',
503
+ `const helper = async (): Promise<Result<object, Error>> =>
504
+ Result.ok({ ok: true });
505
+
506
+ export default helper;
507
+ `
508
+ );
509
+ const caller = writeFile(
510
+ 'caller-default-import.ts',
511
+ `import buildReport from './impl-default-import.js';
512
+
513
+ trail("entity.report", {
514
+ blaze: async (input, ctx) => {
515
+ return buildReport();
516
+ }
517
+ })`
518
+ );
519
+
520
+ const diagnostics = implementationReturnsResult.check(
521
+ readFileSync(caller, 'utf8'),
522
+ caller
523
+ );
524
+
525
+ expect(diagnostics.length).toBe(0);
526
+ });
527
+ });
528
+
529
+ describe('namespace imports and barrel export *', () => {
530
+ test('allows namespace-imported Result helper (import * as ns)', () => {
531
+ writeFile(
532
+ 'impl-namespace.ts',
533
+ `export const helper = async (): Promise<Result<object, Error>> =>
534
+ Result.ok({ ok: true });
535
+ `
536
+ );
537
+ const caller = writeFile(
538
+ 'caller-namespace.ts',
539
+ `import * as ns from './impl-namespace.js';
540
+
541
+ trail("entity.report", {
542
+ blaze: async (input, ctx) => {
543
+ return ns.helper();
544
+ }
545
+ })`
546
+ );
547
+
548
+ const diagnostics = implementationReturnsResult.check(
549
+ readFileSync(caller, 'utf8'),
550
+ caller
551
+ );
552
+
553
+ expect(diagnostics.length).toBe(0);
554
+ });
555
+
556
+ test('flags namespace-imported non-Result member call', () => {
557
+ writeFile(
558
+ 'impl-namespace-mixed.ts',
559
+ `export const helper = async (): Promise<Result<object, Error>> =>
560
+ Result.ok({ ok: true });
561
+
562
+ export const nonResultFn = async () => ({ ok: true });
563
+ `
564
+ );
565
+ const caller = writeFile(
566
+ 'caller-namespace-mixed.ts',
567
+ `import * as ns from './impl-namespace-mixed.js';
568
+
569
+ trail("entity.report", {
570
+ blaze: async (input, ctx) => {
571
+ return ns.nonResultFn();
572
+ }
573
+ })`
574
+ );
575
+
576
+ const diagnostics = implementationReturnsResult.check(
577
+ readFileSync(caller, 'utf8'),
578
+ caller
579
+ );
580
+
581
+ expect(diagnostics.length).toBe(1);
582
+ });
583
+
584
+ test('falls back gracefully on unresolvable namespace import target', () => {
585
+ const caller = writeFile(
586
+ 'caller-namespace-missing.ts',
587
+ `import * as ns from './missing-namespace.js';
588
+
589
+ trail("entity.report", {
590
+ blaze: async (input, ctx) => {
591
+ return ns.helper();
592
+ }
593
+ })`
594
+ );
595
+
596
+ const diagnostics = implementationReturnsResult.check(
597
+ readFileSync(caller, 'utf8'),
598
+ caller
599
+ );
600
+
601
+ expect(diagnostics.length).toBe(1);
602
+ });
603
+
604
+ describe('shadowing by param/const/let', () => {
605
+ test('flags ns.helper() when blaze parameter shadows the namespace import', () => {
606
+ writeFile(
607
+ 'impl-ns-shadow-param.ts',
608
+ `export const helper = async (): Promise<Result<object, Error>> =>
609
+ Result.ok({ ok: true });
610
+ `
611
+ );
612
+ const caller = writeFile(
613
+ 'caller-ns-shadow-param.ts',
614
+ `import * as ns from './impl-ns-shadow-param.js';
615
+
616
+ trail("entity.report", {
617
+ blaze: async (ns, ctx) => {
618
+ return ns.helper(ctx);
619
+ }
620
+ })`
621
+ );
622
+
623
+ const diagnostics = implementationReturnsResult.check(
624
+ readFileSync(caller, 'utf8'),
625
+ caller
626
+ );
627
+
628
+ // The blaze parameter \`ns\` shadows the namespace import; \`ns.helper()\`
629
+ // is a call on the parameter, not on the namespace, so the return
630
+ // must be flagged rather than silently treated as a Result helper.
631
+ expect(diagnostics.length).toBe(1);
632
+ });
633
+
634
+ test('flags ns.helper() when a local const shadows the namespace import', () => {
635
+ writeFile(
636
+ 'impl-ns-shadow-const.ts',
637
+ `export const helper = async (): Promise<Result<object, Error>> =>
638
+ Result.ok({ ok: true });
639
+ `
640
+ );
641
+ const caller = writeFile(
642
+ 'caller-ns-shadow-const.ts',
643
+ `import * as ns from './impl-ns-shadow-const.js';
644
+
645
+ trail("entity.report", {
646
+ blaze: async (input, ctx) => {
647
+ const ns = { helper: () => ({ ok: true }) };
648
+ return ns.helper(input);
649
+ }
650
+ })`
651
+ );
652
+
653
+ const diagnostics = implementationReturnsResult.check(
654
+ readFileSync(caller, 'utf8'),
655
+ caller
656
+ );
657
+
658
+ expect(diagnostics.length).toBe(1);
659
+ });
660
+
661
+ test('flags ns.helper() when a local let shadows the namespace import', () => {
662
+ writeFile(
663
+ 'impl-ns-shadow-let.ts',
664
+ `export const helper = async (): Promise<Result<object, Error>> =>
665
+ Result.ok({ ok: true });
666
+ `
667
+ );
668
+ const caller = writeFile(
669
+ 'caller-ns-shadow-let.ts',
670
+ `import * as ns from './impl-ns-shadow-let.js';
671
+
672
+ trail("entity.report", {
673
+ blaze: async (input, ctx) => {
674
+ let ns = input;
675
+ return ns.helper(input);
676
+ }
677
+ })`
678
+ );
679
+
680
+ const diagnostics = implementationReturnsResult.check(
681
+ readFileSync(caller, 'utf8'),
682
+ caller
683
+ );
684
+
685
+ expect(diagnostics.length).toBe(1);
686
+ });
687
+ });
688
+
689
+ describe('shadowing in for/catch scopes', () => {
690
+ test('flags ns.helper() when a for-init const shadows the namespace import', () => {
691
+ writeFile(
692
+ 'impl-ns-shadow-for-init.ts',
693
+ `export const helper = async (): Promise<Result<object, Error>> =>
694
+ Result.ok({ ok: true });
695
+ `
696
+ );
697
+ const caller = writeFile(
698
+ 'caller-ns-shadow-for-init.ts',
699
+ `import * as ns from './impl-ns-shadow-for-init.js';
700
+
701
+ trail("entity.report", {
702
+ blaze: async (input, ctx) => {
703
+ for (const ns = 0; ns < 1; ns++) {
704
+ return ns.helper(input);
705
+ }
706
+ return Result.ok({});
707
+ }
708
+ })`
709
+ );
710
+
711
+ const diagnostics = implementationReturnsResult.check(
712
+ readFileSync(caller, 'utf8'),
713
+ caller
714
+ );
715
+
716
+ expect(diagnostics.length).toBe(1);
717
+ });
718
+
719
+ test('flags ns.helper() when a for-of binding shadows the namespace import', () => {
720
+ writeFile(
721
+ 'impl-ns-shadow-for-of.ts',
722
+ `export const helper = async (): Promise<Result<object, Error>> =>
723
+ Result.ok({ ok: true });
724
+ `
725
+ );
726
+ const caller = writeFile(
727
+ 'caller-ns-shadow-for-of.ts',
728
+ `import * as ns from './impl-ns-shadow-for-of.js';
729
+
730
+ trail("entity.report", {
731
+ blaze: async (input, ctx) => {
732
+ for (const ns of [1, 2, 3]) {
733
+ return ns.helper(input);
734
+ }
735
+ return Result.ok({});
736
+ }
737
+ })`
738
+ );
739
+
740
+ const diagnostics = implementationReturnsResult.check(
741
+ readFileSync(caller, 'utf8'),
742
+ caller
743
+ );
744
+
745
+ expect(diagnostics.length).toBe(1);
746
+ });
747
+
748
+ test('flags ns.helper() when a catch param shadows the namespace import', () => {
749
+ writeFile(
750
+ 'impl-ns-shadow-catch.ts',
751
+ `export const helper = async (): Promise<Result<object, Error>> =>
752
+ Result.ok({ ok: true });
753
+ `
754
+ );
755
+ const caller = writeFile(
756
+ 'caller-ns-shadow-catch.ts',
757
+ `import * as ns from './impl-ns-shadow-catch.js';
758
+
759
+ trail("entity.report", {
760
+ blaze: async (input, ctx) => {
761
+ try {
762
+ return Result.ok({});
763
+ } catch (ns) {
764
+ return ns.helper(input);
765
+ }
766
+ }
767
+ })`
768
+ );
769
+
770
+ const diagnostics = implementationReturnsResult.check(
771
+ readFileSync(caller, 'utf8'),
772
+ caller
773
+ );
774
+
775
+ expect(diagnostics.length).toBe(1);
776
+ });
777
+
778
+ test('flags ns.helper() when a catch param destructures the namespace name', () => {
779
+ writeFile(
780
+ 'impl-ns-shadow-catch-destructure.ts',
781
+ `export const helper = async (): Promise<Result<object, Error>> =>
782
+ Result.ok({ ok: true });
783
+ `
784
+ );
785
+ const caller = writeFile(
786
+ 'caller-ns-shadow-catch-destructure.ts',
787
+ `import * as ns from './impl-ns-shadow-catch-destructure.js';
788
+
789
+ trail("entity.report", {
790
+ blaze: async (input, ctx) => {
791
+ try {
792
+ return Result.ok({});
793
+ } catch ({ ns }) {
794
+ return ns.helper(input);
795
+ }
796
+ }
797
+ })`
798
+ );
799
+
800
+ const diagnostics = implementationReturnsResult.check(
801
+ readFileSync(caller, 'utf8'),
802
+ caller
803
+ );
804
+
805
+ expect(diagnostics.length).toBe(1);
806
+ });
807
+ });
808
+
809
+ describe('scope-frame coverage (TRL-347)', () => {
810
+ test('flags ns.helper() when a const ns shadow sits in a function-expression blaze body (FunctionBody frame)', () => {
811
+ // Regression: oxc-parser emits `FunctionBody` for regular
812
+ // `function expression() { ... }` bodies, not `BlockStatement`. Without
813
+ // a FunctionBody scope-frame collector, the `const ns = ...` at the
814
+ // top of this body would not push a frame and the module-level
815
+ // namespace import would leak through.
816
+ writeFile(
817
+ 'impl-ns-shadow-fn-expr.ts',
818
+ `export const helper = async (): Promise<Result<object, Error>> =>
819
+ Result.ok({ ok: true });
820
+ `
821
+ );
822
+ const caller = writeFile(
823
+ 'caller-ns-shadow-fn-expr.ts',
824
+ `import * as ns from './impl-ns-shadow-fn-expr.js';
825
+
826
+ trail("entity.report", {
827
+ blaze: async function(input, ctx) {
828
+ const ns = { helper: () => ({ ok: true }) };
829
+ return ns.helper(input);
830
+ }
831
+ })`
832
+ );
833
+
834
+ const diagnostics = implementationReturnsResult.check(
835
+ readFileSync(caller, 'utf8'),
836
+ caller
837
+ );
838
+
839
+ expect(diagnostics.length).toBe(1);
840
+ });
841
+
842
+ test('flags ns.helper() when a hoisted var ns shadows the namespace import', () => {
843
+ // Regression: a `var ns` nested inside a block hoists to the
844
+ // enclosing blaze's function scope. Without function-body-level var
845
+ // hoisting, the namespace import is read as the receiver and the
846
+ // return is silently treated as a Result helper.
847
+ writeFile(
848
+ 'impl-ns-shadow-hoisted-var.ts',
849
+ `export const helper = async (): Promise<Result<object, Error>> =>
850
+ Result.ok({ ok: true });
851
+ `
852
+ );
853
+ const caller = writeFile(
854
+ 'caller-ns-shadow-hoisted-var.ts',
855
+ `import * as ns from './impl-ns-shadow-hoisted-var.js';
856
+
857
+ trail("entity.report", {
858
+ blaze: async (input, ctx) => {
859
+ if (input) {
860
+ var ns = { helper: () => ({ ok: true }) };
861
+ }
862
+ return ns.helper(input);
863
+ }
864
+ })`
865
+ );
866
+
867
+ const diagnostics = implementationReturnsResult.check(
868
+ readFileSync(caller, 'utf8'),
869
+ caller
870
+ );
871
+
872
+ expect(diagnostics.length).toBe(1);
873
+ });
874
+
875
+ test('flags ns.helper() when an unbraced switch case declares a shadowing const', () => {
876
+ // Regression: an unbraced switch case (`case N: const ns = ...;`)
877
+ // does not push a BlockStatement frame. Without the enclosing
878
+ // SwitchStatement collector, the shadow is invisible to the walker.
879
+ writeFile(
880
+ 'impl-ns-shadow-switch.ts',
881
+ `export const helper = async (): Promise<Result<object, Error>> =>
882
+ Result.ok({ ok: true });
883
+ `
884
+ );
885
+ const caller = writeFile(
886
+ 'caller-ns-shadow-switch.ts',
887
+ `import * as ns from './impl-ns-shadow-switch.js';
888
+
889
+ trail("entity.report", {
890
+ blaze: async (input, ctx) => {
891
+ switch (input.kind) {
892
+ case 'a':
893
+ const ns = { helper: () => ({ ok: true }) };
894
+ return ns.helper(input);
895
+ default:
896
+ return Result.ok({});
897
+ }
898
+ }
899
+ })`
900
+ );
901
+
902
+ const diagnostics = implementationReturnsResult.check(
903
+ readFileSync(caller, 'utf8'),
904
+ caller
905
+ );
906
+
907
+ expect(diagnostics.length).toBe(1);
908
+ });
909
+
910
+ test('flags ns.helper() in a sibling switch case when an earlier case declares a shadowing const', () => {
911
+ // Regression: `switch` shares a single lexical scope across every
912
+ // case. A `const ns = ...` in case 'a' shadows the module-level
913
+ // namespace import for every sibling case (including via
914
+ // fall-through). A per-SwitchCase frame would pop the binding when
915
+ // case 'a' ended and miss the shadow in case 'b'.
916
+ writeFile(
917
+ 'impl-ns-shadow-switch-fallthrough.ts',
918
+ `export const helper = async (): Promise<Result<object, Error>> =>
919
+ Result.ok({ ok: true });
920
+ `
921
+ );
922
+ const caller = writeFile(
923
+ 'caller-ns-shadow-switch-fallthrough.ts',
924
+ `import * as ns from './impl-ns-shadow-switch-fallthrough.js';
925
+
926
+ trail("entity.report", {
927
+ blaze: async (input, ctx) => {
928
+ switch (input.kind) {
929
+ case 'a':
930
+ const ns = { helper: () => ({ ok: true }) };
931
+ return ns.helper(input);
932
+ case 'b':
933
+ return ns.helper(input);
934
+ default:
935
+ return Result.ok({});
936
+ }
937
+ }
938
+ })`
939
+ );
940
+
941
+ const diagnostics = implementationReturnsResult.check(
942
+ readFileSync(caller, 'utf8'),
943
+ caller
944
+ );
945
+
946
+ // Both case 'a' and case 'b' should fire — the const declared in
947
+ // case 'a' is visible across the entire switch scope.
948
+ expect(diagnostics.length).toBe(2);
949
+ });
950
+
951
+ test('still resolves ns.helper() correctly when the switch case is braced', () => {
952
+ // Regression: braced cases create a BlockStatement frame nested
953
+ // inside the SwitchStatement frame. The shadow must still apply
954
+ // inside that block, and the sibling case must still see the
955
+ // namespace import (no SwitchStatement-level binding leaks).
956
+ writeFile(
957
+ 'impl-ns-shadow-switch-braced.ts',
958
+ `export const helper = async (): Promise<Result<object, Error>> =>
959
+ Result.ok({ ok: true });
960
+ `
961
+ );
962
+ const caller = writeFile(
963
+ 'caller-ns-shadow-switch-braced.ts',
964
+ `import * as ns from './impl-ns-shadow-switch-braced.js';
965
+
966
+ trail("entity.report", {
967
+ blaze: async (input, ctx) => {
968
+ switch (input.kind) {
969
+ case 'a': {
970
+ const ns = { helper: () => ({ ok: true }) };
971
+ return ns.helper(input);
972
+ }
973
+ case 'b':
974
+ return ns.helper(input);
975
+ default:
976
+ return Result.ok({});
977
+ }
978
+ }
979
+ })`
980
+ );
981
+
982
+ const diagnostics = implementationReturnsResult.check(
983
+ readFileSync(caller, 'utf8'),
984
+ caller
985
+ );
986
+
987
+ // Only the braced case 'a' should fire — the `const ns` is scoped
988
+ // to the inner block and does not leak into case 'b'.
989
+ expect(diagnostics.length).toBe(1);
990
+ });
991
+
992
+ test('flags ns.anything() when the namespace target exports zero Result helpers', () => {
993
+ // Regression: when the target file has no Result-returning exports,
994
+ // `resolveNamespaceSpecifier` used to drop the entry entirely. That
995
+ // made `isNamespaceHelperMemberCall` return false because the
996
+ // namespace binding was absent from the map — the general return-
997
+ // value analysis then fired for the wrong reason. The entry is now
998
+ // recorded with an empty set so the call is correctly identified as
999
+ // a non-Result-helper namespace member call.
1000
+ writeFile(
1001
+ 'impl-ns-empty.ts',
1002
+ `export const nonResultFn = async () => ({ ok: true });
1003
+ `
1004
+ );
1005
+ const caller = writeFile(
1006
+ 'caller-ns-empty.ts',
1007
+ `import * as ns from './impl-ns-empty.js';
1008
+
1009
+ trail("entity.report", {
1010
+ blaze: async (input, ctx) => {
1011
+ return ns.nonResultFn();
1012
+ }
1013
+ })`
1014
+ );
1015
+
1016
+ const diagnostics = implementationReturnsResult.check(
1017
+ readFileSync(caller, 'utf8'),
1018
+ caller
1019
+ );
1020
+
1021
+ expect(diagnostics.length).toBe(1);
1022
+ });
1023
+ });
1024
+
1025
+ test('allows named import through `export * from` barrel', () => {
1026
+ writeFile(
1027
+ 'impl-star.ts',
1028
+ `export const helper = async (): Promise<Result<object, Error>> =>
1029
+ Result.ok({ ok: true });
1030
+ `
1031
+ );
1032
+ writeFile(
1033
+ 'barrel-star.ts',
1034
+ `export * from './impl-star.js';
1035
+ `
1036
+ );
1037
+ const caller = writeFile(
1038
+ 'caller-star.ts',
1039
+ `import { helper } from './barrel-star.js';
1040
+
1041
+ trail("entity.report", {
1042
+ blaze: async (input, ctx) => {
1043
+ return helper();
1044
+ }
1045
+ })`
1046
+ );
1047
+
1048
+ const diagnostics = implementationReturnsResult.check(
1049
+ readFileSync(caller, 'utf8'),
1050
+ caller
1051
+ );
1052
+
1053
+ expect(diagnostics.length).toBe(0);
1054
+ });
1055
+
1056
+ test('falls back gracefully on `export * from` cycle', () => {
1057
+ writeFile(
1058
+ 'star-cycle-a.ts',
1059
+ `export * from './star-cycle-b.js';
1060
+ `
1061
+ );
1062
+ writeFile(
1063
+ 'star-cycle-b.ts',
1064
+ `export * from './star-cycle-a.js';
1065
+ `
1066
+ );
1067
+ const caller = writeFile(
1068
+ 'caller-star-cycle.ts',
1069
+ `import { helper } from './star-cycle-a.js';
1070
+
1071
+ trail("entity.report", {
1072
+ blaze: async (input, ctx) => {
1073
+ return helper();
1074
+ }
1075
+ })`
1076
+ );
1077
+
1078
+ // Should not hang; the helper is not resolvable through the cycle.
1079
+ const diagnostics = implementationReturnsResult.check(
1080
+ readFileSync(caller, 'utf8'),
1081
+ caller
1082
+ );
1083
+
1084
+ expect(diagnostics.length).toBe(1);
1085
+ });
1086
+
1087
+ test('falls back gracefully on re-export cycle', () => {
1088
+ writeFile(
1089
+ 'cycle-a.ts',
1090
+ `export { helper } from './cycle-b.js';
1091
+ `
1092
+ );
1093
+ writeFile(
1094
+ 'cycle-b.ts',
1095
+ `export { helper } from './cycle-a.js';
1096
+ `
1097
+ );
1098
+ const caller = writeFile(
1099
+ 'caller-cycle.ts',
1100
+ `import { helper } from './cycle-a.js';
1101
+
1102
+ trail("entity.report", {
1103
+ blaze: async (input, ctx) => {
1104
+ return helper();
1105
+ }
1106
+ })`
1107
+ );
1108
+
1109
+ // Should not hang. Since the helper's annotation cannot be resolved
1110
+ // through the cycle, it falls back to flagging the return as non-Result.
1111
+ const diagnostics = implementationReturnsResult.check(
1112
+ readFileSync(caller, 'utf8'),
1113
+ caller
1114
+ );
1115
+
1116
+ expect(diagnostics.length).toBe(1);
1117
+ });
1118
+ });
1119
+ });
1120
+
107
1121
  test('allows returning explicitly Result-typed local helpers', () => {
108
1122
  const code = `
109
1123
  const buildDetail = (trailId: string): Result<object, Error> =>