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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (474) hide show
  1. package/.turbo/turbo-lint.log +1 -1
  2. package/CHANGELOG.md +30 -0
  3. package/README.md +31 -20
  4. package/dist/cli.d.ts +19 -2
  5. package/dist/cli.d.ts.map +1 -1
  6. package/dist/cli.js +261 -64
  7. package/dist/cli.js.map +1 -1
  8. package/dist/draft.d.ts +5 -0
  9. package/dist/draft.d.ts.map +1 -0
  10. package/dist/draft.js +16 -0
  11. package/dist/draft.js.map +1 -0
  12. package/dist/drift.d.ts +10 -7
  13. package/dist/drift.d.ts.map +1 -1
  14. package/dist/drift.js +50 -16
  15. package/dist/drift.js.map +1 -1
  16. package/dist/formatters.d.ts +2 -1
  17. package/dist/formatters.d.ts.map +1 -1
  18. package/dist/formatters.js +15 -4
  19. package/dist/formatters.js.map +1 -1
  20. package/dist/index.d.ts +9 -17
  21. package/dist/index.d.ts.map +1 -1
  22. package/dist/index.js +10 -17
  23. package/dist/index.js.map +1 -1
  24. package/dist/rules/ast.d.ts +412 -7
  25. package/dist/rules/ast.d.ts.map +1 -1
  26. package/dist/rules/ast.js +1847 -102
  27. package/dist/rules/ast.js.map +1 -1
  28. package/dist/rules/circular-refs.d.ts +6 -0
  29. package/dist/rules/circular-refs.d.ts.map +1 -0
  30. package/dist/rules/circular-refs.js +83 -0
  31. package/dist/rules/circular-refs.js.map +1 -0
  32. package/dist/rules/context-no-surface-types.d.ts.map +1 -1
  33. package/dist/rules/context-no-surface-types.js +59 -3
  34. package/dist/rules/context-no-surface-types.js.map +1 -1
  35. package/dist/rules/contour-exists.d.ts +7 -0
  36. package/dist/rules/contour-exists.d.ts.map +1 -0
  37. package/dist/rules/contour-exists.js +113 -0
  38. package/dist/rules/contour-exists.js.map +1 -0
  39. package/dist/rules/contour-ids.d.ts +10 -0
  40. package/dist/rules/contour-ids.d.ts.map +1 -0
  41. package/dist/rules/contour-ids.js +12 -0
  42. package/dist/rules/contour-ids.js.map +1 -0
  43. package/dist/rules/cross-declarations.d.ts.map +1 -1
  44. package/dist/rules/cross-declarations.js +171 -57
  45. package/dist/rules/cross-declarations.js.map +1 -1
  46. package/dist/rules/dead-internal-trail.d.ts +3 -0
  47. package/dist/rules/dead-internal-trail.d.ts.map +1 -0
  48. package/dist/rules/dead-internal-trail.js +80 -0
  49. package/dist/rules/dead-internal-trail.js.map +1 -0
  50. package/dist/rules/draft-file-marking.d.ts +6 -0
  51. package/dist/rules/draft-file-marking.d.ts.map +1 -0
  52. package/dist/rules/draft-file-marking.js +87 -0
  53. package/dist/rules/draft-file-marking.js.map +1 -0
  54. package/dist/rules/draft-visible-debt.d.ts +12 -0
  55. package/dist/rules/draft-visible-debt.d.ts.map +1 -0
  56. package/dist/rules/draft-visible-debt.js +50 -0
  57. package/dist/rules/draft-visible-debt.js.map +1 -0
  58. package/dist/rules/error-mapping-completeness.d.ts +13 -0
  59. package/dist/rules/error-mapping-completeness.d.ts.map +1 -0
  60. package/dist/rules/error-mapping-completeness.js +160 -0
  61. package/dist/rules/error-mapping-completeness.js.map +1 -0
  62. package/dist/rules/example-valid.d.ts +6 -0
  63. package/dist/rules/example-valid.d.ts.map +1 -0
  64. package/dist/rules/example-valid.js +203 -0
  65. package/dist/rules/example-valid.js.map +1 -0
  66. package/dist/rules/fires-declarations.d.ts +16 -0
  67. package/dist/rules/fires-declarations.d.ts.map +1 -0
  68. package/dist/rules/fires-declarations.js +444 -0
  69. package/dist/rules/fires-declarations.js.map +1 -0
  70. package/dist/rules/implementation-returns-result.d.ts +9 -0
  71. package/dist/rules/implementation-returns-result.d.ts.map +1 -1
  72. package/dist/rules/implementation-returns-result.js +638 -76
  73. package/dist/rules/implementation-returns-result.js.map +1 -1
  74. package/dist/rules/incomplete-accessor-for-standard-op.d.ts +30 -0
  75. package/dist/rules/incomplete-accessor-for-standard-op.d.ts.map +1 -0
  76. package/dist/rules/incomplete-accessor-for-standard-op.js +226 -0
  77. package/dist/rules/incomplete-accessor-for-standard-op.js.map +1 -0
  78. package/dist/rules/incomplete-crud.d.ts +21 -0
  79. package/dist/rules/incomplete-crud.d.ts.map +1 -0
  80. package/dist/rules/incomplete-crud.js +368 -0
  81. package/dist/rules/incomplete-crud.js.map +1 -0
  82. package/dist/rules/index.d.ts +40 -7
  83. package/dist/rules/index.d.ts.map +1 -1
  84. package/dist/rules/index.js +91 -15
  85. package/dist/rules/index.js.map +1 -1
  86. package/dist/rules/intent-propagation.d.ts +3 -0
  87. package/dist/rules/intent-propagation.d.ts.map +1 -0
  88. package/dist/rules/intent-propagation.js +57 -0
  89. package/dist/rules/intent-propagation.js.map +1 -0
  90. package/dist/rules/missing-reconcile.d.ts +3 -0
  91. package/dist/rules/missing-reconcile.d.ts.map +1 -0
  92. package/dist/rules/missing-reconcile.js +44 -0
  93. package/dist/rules/missing-reconcile.js.map +1 -0
  94. package/dist/rules/missing-visibility.d.ts +3 -0
  95. package/dist/rules/missing-visibility.d.ts.map +1 -0
  96. package/dist/rules/missing-visibility.js +63 -0
  97. package/dist/rules/missing-visibility.js.map +1 -0
  98. package/dist/rules/no-direct-impl-in-route.d.ts.map +1 -1
  99. package/dist/rules/no-direct-impl-in-route.js +0 -3
  100. package/dist/rules/no-direct-impl-in-route.js.map +1 -1
  101. package/dist/rules/no-direct-implementation-call.js +1 -1
  102. package/dist/rules/no-direct-implementation-call.js.map +1 -1
  103. package/dist/rules/no-sync-result-assumption.d.ts.map +1 -1
  104. package/dist/rules/no-sync-result-assumption.js +870 -61
  105. package/dist/rules/no-sync-result-assumption.js.map +1 -1
  106. package/dist/rules/no-throw-in-detour-recover.d.ts +3 -0
  107. package/dist/rules/no-throw-in-detour-recover.d.ts.map +1 -0
  108. package/dist/rules/no-throw-in-detour-recover.js +147 -0
  109. package/dist/rules/no-throw-in-detour-recover.js.map +1 -0
  110. package/dist/rules/no-throw-in-detour-target.d.ts +4 -1
  111. package/dist/rules/no-throw-in-detour-target.d.ts.map +1 -1
  112. package/dist/rules/no-throw-in-detour-target.js +6 -3
  113. package/dist/rules/no-throw-in-detour-target.js.map +1 -1
  114. package/dist/rules/no-throw-in-implementation.d.ts +4 -2
  115. package/dist/rules/no-throw-in-implementation.d.ts.map +1 -1
  116. package/dist/rules/no-throw-in-implementation.js +6 -4
  117. package/dist/rules/no-throw-in-implementation.js.map +1 -1
  118. package/dist/rules/on-references-exist.d.ts +14 -0
  119. package/dist/rules/on-references-exist.d.ts.map +1 -0
  120. package/dist/rules/on-references-exist.js +109 -0
  121. package/dist/rules/on-references-exist.js.map +1 -0
  122. package/dist/rules/orphaned-signal.d.ts +3 -0
  123. package/dist/rules/orphaned-signal.d.ts.map +1 -0
  124. package/dist/rules/orphaned-signal.js +67 -0
  125. package/dist/rules/orphaned-signal.js.map +1 -0
  126. package/dist/rules/permit-governance.d.ts +3 -0
  127. package/dist/rules/permit-governance.d.ts.map +1 -0
  128. package/dist/rules/permit-governance.js +15 -0
  129. package/dist/rules/permit-governance.js.map +1 -0
  130. package/dist/rules/reference-exists.d.ts +6 -0
  131. package/dist/rules/reference-exists.d.ts.map +1 -0
  132. package/dist/rules/reference-exists.js +47 -0
  133. package/dist/rules/reference-exists.js.map +1 -0
  134. package/dist/rules/registry-names.d.ts +8 -0
  135. package/dist/rules/registry-names.d.ts.map +1 -0
  136. package/dist/rules/registry-names.js +83 -0
  137. package/dist/rules/registry-names.js.map +1 -0
  138. package/dist/rules/resource-declarations.d.ts +14 -0
  139. package/dist/rules/resource-declarations.d.ts.map +1 -0
  140. package/dist/rules/resource-declarations.js +413 -0
  141. package/dist/rules/resource-declarations.js.map +1 -0
  142. package/dist/rules/resource-exists.d.ts +6 -0
  143. package/dist/rules/resource-exists.d.ts.map +1 -0
  144. package/dist/rules/resource-exists.js +90 -0
  145. package/dist/rules/resource-exists.js.map +1 -0
  146. package/dist/rules/resource-id-grammar.d.ts +3 -0
  147. package/dist/rules/resource-id-grammar.d.ts.map +1 -0
  148. package/dist/rules/resource-id-grammar.js +39 -0
  149. package/dist/rules/resource-id-grammar.js.map +1 -0
  150. package/dist/rules/specs.d.ts.map +1 -1
  151. package/dist/rules/specs.js +5 -1
  152. package/dist/rules/specs.js.map +1 -1
  153. package/dist/rules/types.d.ts +53 -4
  154. package/dist/rules/types.d.ts.map +1 -1
  155. package/dist/rules/unreachable-detour-shadowing.d.ts +3 -0
  156. package/dist/rules/unreachable-detour-shadowing.d.ts.map +1 -0
  157. package/dist/rules/unreachable-detour-shadowing.js +202 -0
  158. package/dist/rules/unreachable-detour-shadowing.js.map +1 -0
  159. package/dist/rules/valid-describe-refs.d.ts.map +1 -1
  160. package/dist/rules/valid-describe-refs.js +132 -16
  161. package/dist/rules/valid-describe-refs.js.map +1 -1
  162. package/dist/rules/valid-detour-contract.d.ts +3 -0
  163. package/dist/rules/valid-detour-contract.d.ts.map +1 -0
  164. package/dist/rules/valid-detour-contract.js +47 -0
  165. package/dist/rules/valid-detour-contract.js.map +1 -0
  166. package/dist/rules/valid-detour-refs.d.ts.map +1 -1
  167. package/dist/rules/valid-detour-refs.js +73 -82
  168. package/dist/rules/valid-detour-refs.js.map +1 -1
  169. package/dist/rules/warden-export-symmetry.d.ts +7 -0
  170. package/dist/rules/warden-export-symmetry.d.ts.map +1 -0
  171. package/dist/rules/warden-export-symmetry.js +352 -0
  172. package/dist/rules/warden-export-symmetry.js.map +1 -0
  173. package/dist/rules/warden-rules-use-ast.d.ts +17 -0
  174. package/dist/rules/warden-rules-use-ast.d.ts.map +1 -0
  175. package/dist/rules/warden-rules-use-ast.js +778 -0
  176. package/dist/rules/warden-rules-use-ast.js.map +1 -0
  177. package/dist/trails/circular-refs.trail.d.ts +24 -0
  178. package/dist/trails/circular-refs.trail.d.ts.map +1 -0
  179. package/dist/trails/circular-refs.trail.js +29 -0
  180. package/dist/trails/circular-refs.trail.js.map +1 -0
  181. package/dist/trails/context-no-surface-types.trail.d.ts +2 -2
  182. package/dist/trails/context-no-surface-types.trail.d.ts.map +1 -1
  183. package/dist/trails/context-no-trailhead-types.trail.d.ts +2 -2
  184. package/dist/trails/context-no-trailhead-types.trail.d.ts.map +1 -1
  185. package/dist/trails/contour-exists.trail.d.ts +24 -0
  186. package/dist/trails/contour-exists.trail.d.ts.map +1 -0
  187. package/dist/trails/contour-exists.trail.js +21 -0
  188. package/dist/trails/contour-exists.trail.js.map +1 -0
  189. package/dist/trails/cross-declarations.trail.d.ts +2 -2
  190. package/dist/trails/cross-declarations.trail.d.ts.map +1 -1
  191. package/dist/trails/dead-internal-trail.trail.d.ts +24 -0
  192. package/dist/trails/dead-internal-trail.trail.d.ts.map +1 -0
  193. package/dist/trails/dead-internal-trail.trail.js +26 -0
  194. package/dist/trails/dead-internal-trail.trail.js.map +1 -0
  195. package/dist/trails/{provision-declarations.trail.d.ts → draft-file-marking.trail.d.ts} +3 -3
  196. package/dist/trails/draft-file-marking.trail.d.ts.map +1 -0
  197. package/dist/trails/draft-file-marking.trail.js +16 -0
  198. package/dist/trails/draft-file-marking.trail.js.map +1 -0
  199. package/dist/trails/draft-visible-debt.trail.d.ts +13 -0
  200. package/dist/trails/draft-visible-debt.trail.d.ts.map +1 -0
  201. package/dist/trails/draft-visible-debt.trail.js +16 -0
  202. package/dist/trails/draft-visible-debt.trail.js.map +1 -0
  203. package/dist/trails/error-mapping-completeness.trail.d.ts +13 -0
  204. package/dist/trails/error-mapping-completeness.trail.d.ts.map +1 -0
  205. package/dist/trails/error-mapping-completeness.trail.js +29 -0
  206. package/dist/trails/error-mapping-completeness.trail.js.map +1 -0
  207. package/dist/trails/{follow-declarations.trail.d.ts → example-valid.trail.d.ts} +3 -3
  208. package/dist/trails/example-valid.trail.d.ts.map +1 -0
  209. package/dist/trails/example-valid.trail.js +25 -0
  210. package/dist/trails/example-valid.trail.js.map +1 -0
  211. package/dist/trails/fires-declarations.trail.d.ts +13 -0
  212. package/dist/trails/fires-declarations.trail.d.ts.map +1 -0
  213. package/dist/trails/fires-declarations.trail.js +22 -0
  214. package/dist/trails/fires-declarations.trail.js.map +1 -0
  215. package/dist/trails/implementation-returns-result.trail.d.ts +2 -2
  216. package/dist/trails/implementation-returns-result.trail.d.ts.map +1 -1
  217. package/dist/trails/incomplete-accessor-for-standard-op.trail.d.ts +12 -0
  218. package/dist/trails/incomplete-accessor-for-standard-op.trail.d.ts.map +1 -0
  219. package/dist/trails/incomplete-accessor-for-standard-op.trail.js +60 -0
  220. package/dist/trails/incomplete-accessor-for-standard-op.trail.js.map +1 -0
  221. package/dist/trails/incomplete-crud.trail.d.ts +24 -0
  222. package/dist/trails/incomplete-crud.trail.d.ts.map +1 -0
  223. package/dist/trails/incomplete-crud.trail.js +39 -0
  224. package/dist/trails/incomplete-crud.trail.js.map +1 -0
  225. package/dist/trails/index.d.ts +29 -7
  226. package/dist/trails/index.d.ts.map +1 -1
  227. package/dist/trails/index.js +28 -6
  228. package/dist/trails/index.js.map +1 -1
  229. package/dist/trails/intent-propagation.trail.d.ts +24 -0
  230. package/dist/trails/intent-propagation.trail.d.ts.map +1 -0
  231. package/dist/trails/intent-propagation.trail.js +30 -0
  232. package/dist/trails/intent-propagation.trail.js.map +1 -0
  233. package/dist/trails/missing-reconcile.trail.d.ts +24 -0
  234. package/dist/trails/missing-reconcile.trail.d.ts.map +1 -0
  235. package/dist/trails/missing-reconcile.trail.js +33 -0
  236. package/dist/trails/missing-reconcile.trail.js.map +1 -0
  237. package/dist/trails/missing-visibility.trail.d.ts +24 -0
  238. package/dist/trails/missing-visibility.trail.d.ts.map +1 -0
  239. package/dist/trails/missing-visibility.trail.js +22 -0
  240. package/dist/trails/missing-visibility.trail.js.map +1 -0
  241. package/dist/trails/no-direct-impl-in-route.trail.d.ts +2 -2
  242. package/dist/trails/no-direct-impl-in-route.trail.d.ts.map +1 -1
  243. package/dist/trails/no-direct-implementation-call.trail.d.ts +2 -2
  244. package/dist/trails/no-direct-implementation-call.trail.d.ts.map +1 -1
  245. package/dist/trails/no-sync-result-assumption.trail.d.ts +2 -2
  246. package/dist/trails/no-sync-result-assumption.trail.d.ts.map +1 -1
  247. package/dist/trails/no-throw-in-detour-recover.trail.d.ts +13 -0
  248. package/dist/trails/no-throw-in-detour-recover.trail.d.ts.map +1 -0
  249. package/dist/trails/no-throw-in-detour-recover.trail.js +24 -0
  250. package/dist/trails/no-throw-in-detour-recover.trail.js.map +1 -0
  251. package/dist/trails/no-throw-in-detour-target.trail.d.ts +13 -3
  252. package/dist/trails/no-throw-in-detour-target.trail.d.ts.map +1 -1
  253. package/dist/trails/no-throw-in-implementation.trail.d.ts +2 -2
  254. package/dist/trails/no-throw-in-implementation.trail.d.ts.map +1 -1
  255. package/dist/trails/on-references-exist.trail.d.ts +24 -0
  256. package/dist/trails/on-references-exist.trail.d.ts.map +1 -0
  257. package/dist/trails/on-references-exist.trail.js +21 -0
  258. package/dist/trails/on-references-exist.trail.js.map +1 -0
  259. package/dist/trails/orphaned-signal.trail.d.ts +24 -0
  260. package/dist/trails/orphaned-signal.trail.d.ts.map +1 -0
  261. package/dist/trails/orphaned-signal.trail.js +36 -0
  262. package/dist/trails/orphaned-signal.trail.js.map +1 -0
  263. package/dist/trails/permit-governance.trail.d.ts +12 -0
  264. package/dist/trails/permit-governance.trail.d.ts.map +1 -0
  265. package/dist/trails/permit-governance.trail.js +47 -0
  266. package/dist/trails/permit-governance.trail.js.map +1 -0
  267. package/dist/trails/prefer-schema-inference.trail.d.ts +2 -2
  268. package/dist/trails/prefer-schema-inference.trail.d.ts.map +1 -1
  269. package/dist/trails/reference-exists.trail.d.ts +24 -0
  270. package/dist/trails/reference-exists.trail.d.ts.map +1 -0
  271. package/dist/trails/reference-exists.trail.js +25 -0
  272. package/dist/trails/reference-exists.trail.js.map +1 -0
  273. package/dist/trails/resource-declarations.trail.d.ts +13 -0
  274. package/dist/trails/resource-declarations.trail.d.ts.map +1 -0
  275. package/dist/trails/{provision-declarations.trail.js → resource-declarations.trail.js} +7 -7
  276. package/dist/trails/resource-declarations.trail.js.map +1 -0
  277. package/dist/trails/resource-exists.trail.d.ts +24 -0
  278. package/dist/trails/resource-exists.trail.d.ts.map +1 -0
  279. package/dist/trails/{provision-exists.trail.js → resource-exists.trail.js} +8 -8
  280. package/dist/trails/resource-exists.trail.js.map +1 -0
  281. package/dist/trails/resource-id-grammar.trail.d.ts +13 -0
  282. package/dist/trails/resource-id-grammar.trail.d.ts.map +1 -0
  283. package/dist/trails/resource-id-grammar.trail.js +38 -0
  284. package/dist/trails/resource-id-grammar.trail.js.map +1 -0
  285. package/dist/trails/run.d.ts +25 -9
  286. package/dist/trails/run.d.ts.map +1 -1
  287. package/dist/trails/run.js +63 -19
  288. package/dist/trails/run.js.map +1 -1
  289. package/dist/trails/schema.d.ts +28 -3
  290. package/dist/trails/schema.d.ts.map +1 -1
  291. package/dist/trails/schema.js +57 -4
  292. package/dist/trails/schema.js.map +1 -1
  293. package/dist/trails/unreachable-detour-shadowing.trail.d.ts +13 -0
  294. package/dist/trails/unreachable-detour-shadowing.trail.d.ts.map +1 -0
  295. package/dist/trails/unreachable-detour-shadowing.trail.js +44 -0
  296. package/dist/trails/unreachable-detour-shadowing.trail.js.map +1 -0
  297. package/dist/trails/valid-describe-refs.trail.d.ts +12 -3
  298. package/dist/trails/valid-describe-refs.trail.d.ts.map +1 -1
  299. package/dist/trails/valid-detour-contract.trail.d.ts +12 -0
  300. package/dist/trails/valid-detour-contract.trail.d.ts.map +1 -0
  301. package/dist/trails/valid-detour-contract.trail.js +66 -0
  302. package/dist/trails/valid-detour-contract.trail.js.map +1 -0
  303. package/dist/trails/valid-detour-refs.trail.d.ts +13 -3
  304. package/dist/trails/valid-detour-refs.trail.d.ts.map +1 -1
  305. package/dist/trails/warden-export-symmetry.trail.d.ts +13 -0
  306. package/dist/trails/warden-export-symmetry.trail.d.ts.map +1 -0
  307. package/dist/trails/warden-export-symmetry.trail.js +16 -0
  308. package/dist/trails/warden-export-symmetry.trail.js.map +1 -0
  309. package/dist/trails/warden-rules-use-ast.trail.d.ts +13 -0
  310. package/dist/trails/warden-rules-use-ast.trail.d.ts.map +1 -0
  311. package/dist/trails/warden-rules-use-ast.trail.js +41 -0
  312. package/dist/trails/warden-rules-use-ast.trail.js.map +1 -0
  313. package/dist/trails/wrap-rule.d.ts +16 -2
  314. package/dist/trails/wrap-rule.d.ts.map +1 -1
  315. package/dist/trails/wrap-rule.js +71 -11
  316. package/dist/trails/wrap-rule.js.map +1 -1
  317. package/package.json +7 -4
  318. package/src/__tests__/ast.test.ts +613 -0
  319. package/src/__tests__/circular-refs.test.ts +121 -0
  320. package/src/__tests__/cli.test.ts +360 -32
  321. package/src/__tests__/contour-exists.test.ts +203 -0
  322. package/src/__tests__/cross-declarations.test.ts +245 -0
  323. package/src/__tests__/dead-internal-trail.test.ts +81 -0
  324. package/src/__tests__/draft-rules-context.test.ts +150 -0
  325. package/src/__tests__/drift.test.ts +75 -5
  326. package/src/__tests__/error-mapping-completeness.test.ts +56 -0
  327. package/src/__tests__/example-valid.test.ts +101 -0
  328. package/src/__tests__/fires-declarations-param-destructure.test.ts +54 -0
  329. package/src/__tests__/fires-declarations.test.ts +652 -0
  330. package/src/__tests__/formatters.test.ts +2 -2
  331. package/src/__tests__/implementation-returns-result.test.ts +1016 -2
  332. package/src/__tests__/incomplete-accessor-for-standard-op.test.ts +337 -0
  333. package/src/__tests__/incomplete-crud.test.ts +498 -0
  334. package/src/__tests__/intent-propagation.test.ts +116 -0
  335. package/src/__tests__/missing-reconcile.test.ts +154 -0
  336. package/src/__tests__/missing-visibility.test.ts +108 -0
  337. package/src/__tests__/no-sync-result-assumption.test.ts +870 -39
  338. package/src/__tests__/no-throw-in-detour-recover.test.ts +93 -0
  339. package/src/__tests__/no-throw-in-implementation.test.ts +88 -0
  340. package/src/__tests__/on-references-exist.test.ts +151 -0
  341. package/src/__tests__/orphaned-signal.test.ts +137 -0
  342. package/src/__tests__/permit-governance.test.ts +66 -0
  343. package/src/__tests__/reference-exists.test.ts +281 -0
  344. package/src/__tests__/resource-declarations.test.ts +448 -0
  345. package/src/__tests__/resource-exists.test.ts +122 -0
  346. package/src/__tests__/resource-id-grammar.test.ts +50 -0
  347. package/src/__tests__/rules.test.ts +17 -77
  348. package/src/__tests__/topo-aware-rule.test.ts +257 -0
  349. package/src/__tests__/trails.test.ts +2 -2
  350. package/src/__tests__/unreachable-detour-shadowing.test.ts +128 -0
  351. package/src/__tests__/valid-describe-refs.test.ts +183 -0
  352. package/src/__tests__/valid-detour-contract.test.ts +86 -0
  353. package/src/__tests__/warden-export-symmetry.test.ts +251 -0
  354. package/src/__tests__/warden-rules-use-ast.test.ts +468 -0
  355. package/src/__tests__/wrap-rule.test.ts +3 -3
  356. package/src/cli.ts +458 -91
  357. package/src/draft.ts +22 -0
  358. package/src/drift.ts +63 -21
  359. package/src/formatters.ts +15 -4
  360. package/src/index.ts +62 -23
  361. package/src/rules/ast.ts +2715 -119
  362. package/src/rules/circular-refs.ts +154 -0
  363. package/src/rules/{context-no-trailhead-types.ts → context-no-surface-types.ts} +72 -12
  364. package/src/rules/contour-exists.ts +251 -0
  365. package/src/rules/contour-ids.ts +15 -0
  366. package/src/rules/cross-declarations.ts +277 -69
  367. package/src/rules/dead-internal-trail.ts +141 -0
  368. package/src/rules/draft-file-marking.ts +160 -0
  369. package/src/rules/draft-visible-debt.ts +87 -0
  370. package/src/rules/error-mapping-completeness.ts +273 -0
  371. package/src/rules/example-valid.ts +401 -0
  372. package/src/rules/fires-declarations.ts +609 -0
  373. package/src/rules/implementation-returns-result.ts +1042 -122
  374. package/src/rules/incomplete-accessor-for-standard-op.ts +315 -0
  375. package/src/rules/incomplete-crud.ts +579 -0
  376. package/src/rules/index.ts +95 -16
  377. package/src/rules/intent-propagation.ts +142 -0
  378. package/src/rules/missing-reconcile.ts +98 -0
  379. package/src/rules/missing-visibility.ts +110 -0
  380. package/src/rules/no-direct-impl-in-route.ts +0 -4
  381. package/src/rules/no-direct-implementation-call.ts +1 -1
  382. package/src/rules/no-sync-result-assumption.ts +1134 -96
  383. package/src/rules/no-throw-in-detour-recover.ts +225 -0
  384. package/src/rules/no-throw-in-implementation.ts +6 -4
  385. package/src/rules/on-references-exist.ts +194 -0
  386. package/src/rules/orphaned-signal.ts +150 -0
  387. package/src/rules/permit-governance.ts +25 -0
  388. package/src/rules/reference-exists.ts +98 -0
  389. package/src/rules/registry-names.ts +83 -0
  390. package/src/rules/{provision-declarations.ts → resource-declarations.ts} +208 -138
  391. package/src/rules/{provision-exists.ts → resource-exists.ts} +48 -51
  392. package/src/rules/resource-id-grammar.ts +65 -0
  393. package/src/rules/specs.ts +5 -1
  394. package/src/rules/types.ts +57 -4
  395. package/src/rules/unreachable-detour-shadowing.ts +375 -0
  396. package/src/rules/valid-describe-refs.ts +160 -32
  397. package/src/rules/valid-detour-contract.ts +78 -0
  398. package/src/rules/warden-export-symmetry.ts +533 -0
  399. package/src/rules/warden-rules-use-ast.ts +996 -0
  400. package/src/trails/circular-refs.trail.ts +29 -0
  401. package/src/trails/{context-no-trailhead-types.trail.ts → context-no-surface-types.trail.ts} +4 -4
  402. package/src/trails/contour-exists.trail.ts +21 -0
  403. package/src/trails/dead-internal-trail.trail.ts +26 -0
  404. package/src/trails/draft-file-marking.trail.ts +16 -0
  405. package/src/trails/draft-visible-debt.trail.ts +16 -0
  406. package/src/trails/error-mapping-completeness.trail.ts +29 -0
  407. package/src/trails/example-valid.trail.ts +25 -0
  408. package/src/trails/fires-declarations.trail.ts +22 -0
  409. package/src/trails/incomplete-accessor-for-standard-op.trail.ts +76 -0
  410. package/src/trails/incomplete-crud.trail.ts +39 -0
  411. package/src/trails/index.ts +40 -7
  412. package/src/trails/intent-propagation.trail.ts +30 -0
  413. package/src/trails/missing-reconcile.trail.ts +33 -0
  414. package/src/trails/missing-visibility.trail.ts +22 -0
  415. package/src/trails/no-throw-in-detour-recover.trail.ts +24 -0
  416. package/src/trails/on-references-exist.trail.ts +21 -0
  417. package/src/trails/orphaned-signal.trail.ts +36 -0
  418. package/src/trails/permit-governance.trail.ts +51 -0
  419. package/src/trails/reference-exists.trail.ts +25 -0
  420. package/src/trails/{provision-declarations.trail.ts → resource-declarations.trail.ts} +6 -6
  421. package/src/trails/{provision-exists.trail.ts → resource-exists.trail.ts} +7 -7
  422. package/src/trails/resource-id-grammar.trail.ts +39 -0
  423. package/src/trails/run.ts +121 -24
  424. package/src/trails/schema.ts +66 -4
  425. package/src/trails/unreachable-detour-shadowing.trail.ts +45 -0
  426. package/src/trails/valid-detour-contract.trail.ts +71 -0
  427. package/src/trails/warden-export-symmetry.trail.ts +16 -0
  428. package/src/trails/warden-rules-use-ast.trail.ts +45 -0
  429. package/src/trails/wrap-rule.ts +104 -12
  430. package/tsconfig.tests.json +10 -0
  431. package/tsconfig.tsbuildinfo +1 -1
  432. package/dist/rules/follow-declarations.d.ts +0 -13
  433. package/dist/rules/follow-declarations.d.ts.map +0 -1
  434. package/dist/rules/follow-declarations.js +0 -264
  435. package/dist/rules/follow-declarations.js.map +0 -1
  436. package/dist/rules/provision-declarations.d.ts +0 -14
  437. package/dist/rules/provision-declarations.d.ts.map +0 -1
  438. package/dist/rules/provision-declarations.js +0 -344
  439. package/dist/rules/provision-declarations.js.map +0 -1
  440. package/dist/rules/provision-exists.d.ts +0 -6
  441. package/dist/rules/provision-exists.d.ts.map +0 -1
  442. package/dist/rules/provision-exists.js +0 -89
  443. package/dist/rules/provision-exists.js.map +0 -1
  444. package/dist/rules/service-declarations.d.ts +0 -16
  445. package/dist/rules/service-declarations.d.ts.map +0 -1
  446. package/dist/rules/service-declarations.js +0 -346
  447. package/dist/rules/service-declarations.js.map +0 -1
  448. package/dist/rules/service-exists.d.ts +0 -8
  449. package/dist/rules/service-exists.d.ts.map +0 -1
  450. package/dist/rules/service-exists.js +0 -91
  451. package/dist/rules/service-exists.js.map +0 -1
  452. package/dist/trails/follow-declarations.trail.d.ts.map +0 -1
  453. package/dist/trails/follow-declarations.trail.js +0 -22
  454. package/dist/trails/follow-declarations.trail.js.map +0 -1
  455. package/dist/trails/provision-declarations.trail.d.ts.map +0 -1
  456. package/dist/trails/provision-declarations.trail.js.map +0 -1
  457. package/dist/trails/provision-exists.trail.d.ts +0 -15
  458. package/dist/trails/provision-exists.trail.d.ts.map +0 -1
  459. package/dist/trails/provision-exists.trail.js.map +0 -1
  460. package/dist/trails/service-declarations.trail.d.ts +0 -26
  461. package/dist/trails/service-declarations.trail.d.ts.map +0 -1
  462. package/dist/trails/service-declarations.trail.js +0 -27
  463. package/dist/trails/service-declarations.trail.js.map +0 -1
  464. package/dist/trails/service-exists.trail.d.ts +0 -32
  465. package/dist/trails/service-exists.trail.d.ts.map +0 -1
  466. package/dist/trails/service-exists.trail.js +0 -29
  467. package/dist/trails/service-exists.trail.js.map +0 -1
  468. package/src/__tests__/no-throw-in-detour-target.test.ts +0 -78
  469. package/src/__tests__/provision-declarations.test.ts +0 -318
  470. package/src/__tests__/provision-exists.test.ts +0 -122
  471. package/src/rules/no-throw-in-detour-target.ts +0 -150
  472. package/src/rules/valid-detour-refs.ts +0 -187
  473. package/src/trails/no-throw-in-detour-target.trail.ts +0 -20
  474. package/src/trails/valid-detour-refs.trail.ts +0 -24
@@ -0,0 +1,652 @@
1
+ import { describe, expect, test } from 'bun:test';
2
+
3
+ import { firesDeclarations } from '../rules/fires-declarations.js';
4
+
5
+ const TEST_FILE = 'test.ts';
6
+
7
+ describe('fires-declarations', () => {
8
+ describe('clean cases', () => {
9
+ test('declared and called match exactly', () => {
10
+ const code = `
11
+ import { trail, Result } from '@ontrails/core';
12
+ const t = trail('onboard', {
13
+ fires: ['entity.created', 'audit.logged'],
14
+ input: z.object({ name: z.string() }),
15
+ blaze: async (input, ctx) => {
16
+ await ctx.fire('entity.created', { name: input.name });
17
+ await ctx.fire('audit.logged', { actor: input.name });
18
+ return Result.ok({});
19
+ },
20
+ });
21
+ `;
22
+
23
+ const diagnostics = firesDeclarations.check(code, TEST_FILE);
24
+
25
+ expect(diagnostics.length).toBe(0);
26
+ });
27
+
28
+ test('optional-chained ctx.fire?.() call matching declaration is clean', () => {
29
+ const code = `
30
+ trail('optionalChain', {
31
+ fires: ['declared.signal'],
32
+ input: z.object({ name: z.string() }),
33
+ blaze: async (input, ctx) => {
34
+ await ctx.fire?.('declared.signal', { name: input.name });
35
+ return Result.ok({});
36
+ },
37
+ });
38
+ `;
39
+
40
+ const diagnostics = firesDeclarations.check(code, TEST_FILE);
41
+ // Optional-chain invocation of ctx.fire must not produce false positives.
42
+ // Locks in current behavior so the dogfood demo's `ctx.fire?.(...)` stays
43
+ // clean regardless of future AST walker changes.
44
+ expect(diagnostics.length).toBe(0);
45
+ });
46
+
47
+ test('no fires declaration and no ctx.fire() calls', () => {
48
+ const code = `
49
+ trail('simple', {
50
+ input: z.object({ name: z.string() }),
51
+ blaze: async (input, ctx) => {
52
+ return Result.ok({ greeting: 'hello ' + input.name });
53
+ },
54
+ });
55
+ `;
56
+
57
+ const diagnostics = firesDeclarations.check(code, TEST_FILE);
58
+
59
+ expect(diagnostics.length).toBe(0);
60
+ });
61
+ });
62
+
63
+ describe('error cases', () => {
64
+ test('called but not declared produces error', () => {
65
+ const code = `
66
+ trail('onboard', {
67
+ input: z.object({ name: z.string() }),
68
+ blaze: async (input, ctx) => {
69
+ await ctx.fire('entity.created', { name: input.name });
70
+ return Result.ok({});
71
+ },
72
+ });
73
+ `;
74
+
75
+ const diagnostics = firesDeclarations.check(code, TEST_FILE);
76
+
77
+ expect(diagnostics.length).toBe(1);
78
+ expect(diagnostics[0]?.severity).toBe('error');
79
+ expect(diagnostics[0]?.rule).toBe('fires-declarations');
80
+ expect(diagnostics[0]?.message).toContain("ctx.fire('entity.created')");
81
+ expect(diagnostics[0]?.message).toContain('not declared in fires');
82
+ });
83
+ });
84
+
85
+ describe('warn cases', () => {
86
+ test('declared but not called produces warning', () => {
87
+ const code = `
88
+ trail('onboard', {
89
+ fires: ['entity.created', 'audit.logged'],
90
+ blaze: async (input, ctx) => {
91
+ await ctx.fire('entity.created', { name: input.name });
92
+ return Result.ok({});
93
+ },
94
+ });
95
+ `;
96
+
97
+ const diagnostics = firesDeclarations.check(code, TEST_FILE);
98
+
99
+ expect(diagnostics.length).toBe(1);
100
+ expect(diagnostics[0]?.severity).toBe('warn');
101
+ expect(diagnostics[0]?.rule).toBe('fires-declarations');
102
+ expect(diagnostics[0]?.message).toContain(
103
+ "'audit.logged' declared in fires"
104
+ );
105
+ expect(diagnostics[0]?.message).toContain('never called');
106
+ });
107
+ });
108
+
109
+ describe('single-object overload', () => {
110
+ test('recognizes trail({ id, fires, blaze }) form', () => {
111
+ const code = `
112
+ trail({
113
+ id: 'onboard',
114
+ fires: ['entity.created'],
115
+ input: z.object({ name: z.string() }),
116
+ blaze: async (input, ctx) => {
117
+ await ctx.fire('entity.created', { name: input.name });
118
+ return Result.ok({});
119
+ },
120
+ });
121
+ `;
122
+
123
+ const diagnostics = firesDeclarations.check(code, TEST_FILE);
124
+
125
+ expect(diagnostics.length).toBe(0);
126
+ });
127
+
128
+ test('detects undeclared fires in single-object form', () => {
129
+ const code = `
130
+ trail({
131
+ id: 'onboard',
132
+ input: z.object({ name: z.string() }),
133
+ blaze: async (input, ctx) => {
134
+ await ctx.fire('entity.created', { name: input.name });
135
+ return Result.ok({});
136
+ },
137
+ });
138
+ `;
139
+
140
+ const diagnostics = firesDeclarations.check(code, TEST_FILE);
141
+
142
+ expect(diagnostics.length).toBe(1);
143
+ expect(diagnostics[0]?.severity).toBe('error');
144
+ expect(diagnostics[0]?.message).toContain("'entity.created'");
145
+ });
146
+ });
147
+
148
+ describe('context parameter naming', () => {
149
+ test('recognizes context.fire() when second param is named context', () => {
150
+ const code = `
151
+ trail('onboard', {
152
+ fires: ['entity.created'],
153
+ input: z.object({ name: z.string() }),
154
+ blaze: async (input, context) => {
155
+ await context.fire('entity.created', { name: input.name });
156
+ return Result.ok({});
157
+ },
158
+ });
159
+ `;
160
+
161
+ const diagnostics = firesDeclarations.check(code, TEST_FILE);
162
+
163
+ expect(diagnostics.length).toBe(0);
164
+ });
165
+
166
+ test('blaze with no second parameter: unrelated closure ctx.fire is not tracked', () => {
167
+ const code = `
168
+ const ctx = { fire: (_: string) => {} };
169
+ trail('noCtxParam', {
170
+ blaze: async (input) => {
171
+ ctx.fire('closure.signal');
172
+ return Result.ok({});
173
+ },
174
+ });
175
+ `;
176
+
177
+ const diagnostics = firesDeclarations.check(code, TEST_FILE);
178
+ // The blaze has no context parameter, so `ctx` in the body refers to
179
+ // some unrelated closure identifier, not a trail context. It must not
180
+ // be tracked — no diagnostics.
181
+ expect(diagnostics.length).toBe(0);
182
+ });
183
+
184
+ test('custom-named context param: only that name is tracked', () => {
185
+ const code = `
186
+ const ctx = { fire: (_: string) => {} };
187
+ trail('customCtx', {
188
+ fires: ['declared.id'],
189
+ blaze: async (input, c) => {
190
+ await c.fire('declared.id', {});
191
+ ctx.fire('whatever');
192
+ return Result.ok({});
193
+ },
194
+ });
195
+ `;
196
+
197
+ const diagnostics = firesDeclarations.check(code, TEST_FILE);
198
+ // `c.fire('declared.id')` matches the declaration. The unrelated
199
+ // closure `ctx.fire('whatever')` must not be flagged because `ctx` is
200
+ // not the trail context — only `c` is.
201
+ expect(diagnostics.length).toBe(0);
202
+ });
203
+
204
+ test('custom-named context param: undeclared call via that name is flagged', () => {
205
+ const code = `
206
+ trail('customCtxUndeclared', {
207
+ fires: ['declared.id'],
208
+ blaze: async (input, c) => {
209
+ await c.fire('undeclared.id', {});
210
+ return Result.ok({});
211
+ },
212
+ });
213
+ `;
214
+
215
+ const diagnostics = firesDeclarations.check(code, TEST_FILE);
216
+ const undeclared = diagnostics.filter((d) =>
217
+ d.message.includes("'undeclared.id'")
218
+ );
219
+ expect(undeclared.length).toBe(1);
220
+ expect(undeclared[0]?.severity).toBe('error');
221
+ });
222
+
223
+ test('recognizes destructured fire() calls', () => {
224
+ const code = `
225
+ trail('onboard', {
226
+ fires: ['entity.created'],
227
+ input: z.object({ name: z.string() }),
228
+ blaze: async (input, ctx) => {
229
+ const { fire } = ctx;
230
+ await fire('entity.created', { name: input.name });
231
+ return Result.ok({});
232
+ },
233
+ });
234
+ `;
235
+
236
+ const diagnostics = firesDeclarations.check(code, TEST_FILE);
237
+
238
+ expect(diagnostics.length).toBe(0);
239
+ });
240
+ });
241
+
242
+ describe('nested run false positives', () => {
243
+ test('meta.blaze with phantom fire does not trigger false positives', () => {
244
+ const code = `
245
+ trail('onboard', {
246
+ fires: ['entity.created'],
247
+ input: z.object({ name: z.string() }),
248
+ meta: { blaze: async () => ctx.fire('phantom') },
249
+ blaze: async (input, ctx) => {
250
+ await ctx.fire('entity.created', { name: input.name });
251
+ return Result.ok({});
252
+ },
253
+ });
254
+ `;
255
+
256
+ const diagnostics = firesDeclarations.check(code, TEST_FILE);
257
+
258
+ expect(diagnostics.length).toBe(0);
259
+ });
260
+ });
261
+
262
+ describe('identifier resolution in fires arrays', () => {
263
+ test('resolves const identifiers in fires array', () => {
264
+ const code = `
265
+ const ENTITY_CREATED = 'entity.created';
266
+ trail('onboard', {
267
+ fires: [ENTITY_CREATED],
268
+ input: z.object({ name: z.string() }),
269
+ blaze: async (input, ctx) => {
270
+ await ctx.fire('entity.created', { name: input.name });
271
+ return Result.ok({});
272
+ },
273
+ });
274
+ `;
275
+
276
+ const diagnostics = firesDeclarations.check(code, TEST_FILE);
277
+
278
+ expect(diagnostics.length).toBe(0);
279
+ });
280
+ });
281
+
282
+ describe('edge cases', () => {
283
+ test('dynamic fire IDs are skipped', () => {
284
+ const code = `
285
+ trail('dispatch', {
286
+ fires: ['entity.created'],
287
+ blaze: async (input, ctx) => {
288
+ const signalId = input.target;
289
+ await ctx.fire(signalId, input);
290
+ await ctx.fire('entity.created', input);
291
+ return Result.ok({});
292
+ },
293
+ });
294
+ `;
295
+
296
+ const diagnostics = firesDeclarations.check(code, TEST_FILE);
297
+
298
+ expect(diagnostics.length).toBe(0);
299
+ });
300
+
301
+ test('multiple trails in one file are validated independently', () => {
302
+ const code = `
303
+ trail('alpha', {
304
+ fires: ['shared.signal'],
305
+ blaze: async (input, ctx) => {
306
+ await ctx.fire('shared.signal', input);
307
+ return Result.ok({});
308
+ },
309
+ });
310
+
311
+ trail('beta', {
312
+ blaze: async (input, ctx) => {
313
+ await ctx.fire('undeclared.signal', input);
314
+ return Result.ok({});
315
+ },
316
+ });
317
+ `;
318
+
319
+ const diagnostics = firesDeclarations.check(code, TEST_FILE);
320
+
321
+ expect(diagnostics.length).toBe(1);
322
+ expect(diagnostics[0]?.message).toContain('Trail "beta"');
323
+ expect(diagnostics[0]?.message).toContain("'undeclared.signal'");
324
+ expect(diagnostics[0]?.severity).toBe('error');
325
+ });
326
+
327
+ test('skips test files', () => {
328
+ const code = `
329
+ trail('onboard', {
330
+ blaze: async (input, ctx) => {
331
+ await ctx.fire('entity.created', input);
332
+ return Result.ok({});
333
+ },
334
+ });
335
+ `;
336
+
337
+ const diagnostics = firesDeclarations.check(
338
+ code,
339
+ 'src/__tests__/trails.test.ts'
340
+ );
341
+
342
+ expect(diagnostics.length).toBe(0);
343
+ });
344
+
345
+ test('both wrong: called and undeclared, declared and unused', () => {
346
+ const code = `
347
+ trail('mixed', {
348
+ fires: ['declared.only'],
349
+ blaze: async (input, ctx) => {
350
+ await ctx.fire('called.only', input);
351
+ return Result.ok({});
352
+ },
353
+ });
354
+ `;
355
+
356
+ const diagnostics = firesDeclarations.check(code, TEST_FILE);
357
+
358
+ expect(diagnostics.length).toBe(2);
359
+ const errors = diagnostics.filter((d) => d.severity === 'error');
360
+ const warns = diagnostics.filter((d) => d.severity === 'warn');
361
+ expect(errors.length).toBe(1);
362
+ expect(warns.length).toBe(1);
363
+ });
364
+ });
365
+
366
+ describe('bare fire() helpers', () => {
367
+ test('unrelated local fire() helper is not flagged', () => {
368
+ const code = `
369
+ import { trail, Result } from '@ontrails/core';
370
+ const fire = (x: number) => x * 2;
371
+ const t = trail('calc', {
372
+ input: z.object({ n: z.number() }),
373
+ blaze: async (input, ctx) => {
374
+ const doubled = fire(input.n);
375
+ return Result.ok({ doubled });
376
+ },
377
+ });
378
+ `;
379
+
380
+ const diagnostics = firesDeclarations.check(code, TEST_FILE);
381
+
382
+ // Neither declared nor "called" from the trail's perspective.
383
+ expect(diagnostics.length).toBe(0);
384
+ });
385
+
386
+ test('destructured { fire } from ctx is tracked', () => {
387
+ const code = `
388
+ trail('onboard', {
389
+ fires: ['entity.created'],
390
+ blaze: async (input, ctx) => {
391
+ const { fire } = ctx;
392
+ await fire('entity.created', { name: input.name });
393
+ return Result.ok({});
394
+ },
395
+ });
396
+ `;
397
+
398
+ const diagnostics = firesDeclarations.check(code, TEST_FILE);
399
+ expect(diagnostics.length).toBe(0);
400
+ });
401
+ test('nested-scope destructure does not leak to outer blaze scope', () => {
402
+ const code = `
403
+ trail('outer', {
404
+ blaze: async (input, ctx) => {
405
+ function nested() {
406
+ const { fire } = ctx;
407
+ return fire;
408
+ }
409
+ fire('outer.signal');
410
+ return Result.ok({});
411
+ },
412
+ });
413
+ `;
414
+
415
+ const diagnostics = firesDeclarations.check(code, TEST_FILE);
416
+ // Top-level fire('outer.signal') should NOT be treated as a ctx call —
417
+ // the nested destructure must not leak out.
418
+ expect(diagnostics.length).toBe(0);
419
+ });
420
+
421
+ test('top-level destructure is still tracked (regression check)', () => {
422
+ const code = `
423
+ trail('tracked', {
424
+ blaze: async (input, ctx) => {
425
+ const { fire } = ctx;
426
+ await fire('undeclared.signal', {});
427
+ return Result.ok({});
428
+ },
429
+ });
430
+ `;
431
+
432
+ const diagnostics = firesDeclarations.check(code, TEST_FILE);
433
+ expect(diagnostics.length).toBe(1);
434
+ expect(diagnostics[0]?.severity).toBe('error');
435
+ expect(diagnostics[0]?.message).toContain("'undeclared.signal'");
436
+ });
437
+
438
+ test('nested function parameter shadowing fire is not flagged', () => {
439
+ const code = `
440
+ trail('shadowFire', {
441
+ blaze: async (_, ctx) => {
442
+ const { fire } = ctx;
443
+ function nested(fire) {
444
+ fire('shadow.only');
445
+ }
446
+ nested(() => {});
447
+ return Result.ok({});
448
+ },
449
+ });
450
+ `;
451
+
452
+ const diagnostics = firesDeclarations.check(code, TEST_FILE);
453
+ // The nested parameter shadows the outer destructured `fire`, so the
454
+ // call inside `nested` must not be treated as a trail fire.
455
+ expect(diagnostics.length).toBe(0);
456
+ });
457
+
458
+ test('nested function parameter shadowing ctx is not flagged', () => {
459
+ const code = `
460
+ trail('shadowCtx', {
461
+ blaze: async (_, ctx) => {
462
+ function nested(ctx) {
463
+ ctx.fire('shadow.ctx');
464
+ }
465
+ nested({});
466
+ return Result.ok({});
467
+ },
468
+ });
469
+ `;
470
+
471
+ const diagnostics = firesDeclarations.check(code, TEST_FILE);
472
+ expect(diagnostics.length).toBe(0);
473
+ });
474
+
475
+ test('top-level ctx.fire with matching declaration still clean (regression)', () => {
476
+ const code = `
477
+ trail('regression', {
478
+ fires: ['declared.signal'],
479
+ blaze: async (input, ctx) => {
480
+ await ctx.fire('declared.signal', {});
481
+ return Result.ok({});
482
+ },
483
+ });
484
+ `;
485
+
486
+ const diagnostics = firesDeclarations.check(code, TEST_FILE);
487
+ expect(diagnostics.length).toBe(0);
488
+ });
489
+
490
+ test('let { fire: emit } = ctx is not tracked (precision tradeoff)', () => {
491
+ const code = `
492
+ trail('letDestructure', {
493
+ blaze: async (input, ctx) => {
494
+ let { fire: emit } = ctx;
495
+ emit('some.id');
496
+ return Result.ok({});
497
+ },
498
+ });
499
+ `;
500
+
501
+ const diagnostics = firesDeclarations.check(code, TEST_FILE);
502
+ // Only `const` destructures are tracked. `let` / `var` allow
503
+ // reassignment which this flow-insensitive walker cannot follow, so
504
+ // `emit` is treated as unrelated and no diagnostic is produced.
505
+ expect(diagnostics.length).toBe(0);
506
+ });
507
+
508
+ test('destructured { fire: emit } alias from ctx is tracked', () => {
509
+ const code = `
510
+ trail('onboard', {
511
+ fires: ['entity.created'],
512
+ blaze: async (input, ctx) => {
513
+ const { fire: emit } = ctx;
514
+ await emit('entity.created', { name: input.name });
515
+ return Result.ok({});
516
+ },
517
+ });
518
+ `;
519
+
520
+ const diagnostics = firesDeclarations.check(code, TEST_FILE);
521
+ expect(diagnostics.length).toBe(0);
522
+ });
523
+ });
524
+
525
+ describe('object-form fires declarations', () => {
526
+ test('Signal value reference downgrades undeclared to warn', () => {
527
+ const code = `
528
+ import { trail, signal, Result } from '@ontrails/core';
529
+ const orderPlaced = signal('order.placed', { payload: z.object({}) });
530
+ trail('checkout', {
531
+ fires: [orderPlaced],
532
+ blaze: async (input, ctx) => {
533
+ await ctx.fire('order.placed', {});
534
+ return Result.ok({});
535
+ },
536
+ });
537
+ `;
538
+
539
+ const diagnostics = firesDeclarations.check(code, TEST_FILE);
540
+ // Object-form makes the declared set unresolvable — downgrade undeclared
541
+ // to warn rather than silently suppressing; runtime normalization in
542
+ // trail() may cover the call.
543
+ expect(diagnostics.length).toBe(1);
544
+ expect(diagnostics[0]?.severity).toBe('warn');
545
+ expect(diagnostics[0]?.message).toContain('object-form fires entries');
546
+ });
547
+
548
+ test('genuinely undeclared string call alongside Signal value is downgraded to warn', () => {
549
+ const code = `
550
+ import { trail, signal, Result } from '@ontrails/core';
551
+ const orderPlaced = signal('order.placed', { payload: z.object({}) });
552
+ trail('checkout', {
553
+ fires: [orderPlaced],
554
+ blaze: async (input, ctx) => {
555
+ await ctx.fire('audit.logged', {});
556
+ return Result.ok({});
557
+ },
558
+ });
559
+ `;
560
+
561
+ const diagnostics = firesDeclarations.check(code, TEST_FILE);
562
+ // Cannot statically prove 'audit.logged' isn't covered by the object-form
563
+ // entry, but also cannot stay silent — warn with disclaimer.
564
+ const undeclared = diagnostics.filter((d) =>
565
+ d.message.includes("'audit.logged'")
566
+ );
567
+ expect(undeclared.length).toBe(1);
568
+ expect(undeclared[0]?.severity).toBe('warn');
569
+ expect(undeclared[0]?.message).toContain(
570
+ 'may be declared via object-form fires entries'
571
+ );
572
+ });
573
+
574
+ test('mixed string + Signal value — resolved string still matches, unresolved warns', () => {
575
+ const code = `
576
+ import { trail, signal, Result } from '@ontrails/core';
577
+ const orderPlaced = signal('order.placed', { payload: z.object({}) });
578
+ trail('checkout', {
579
+ fires: ['audit.logged', orderPlaced],
580
+ blaze: async (input, ctx) => {
581
+ await ctx.fire('order.placed', {});
582
+ await ctx.fire('audit.logged', {});
583
+ return Result.ok({});
584
+ },
585
+ });
586
+ `;
587
+
588
+ const diagnostics = firesDeclarations.check(code, TEST_FILE);
589
+ // 'audit.logged' resolves from the string literal; 'order.placed' can't
590
+ // be resolved statically so it downgrades to warn.
591
+ expect(diagnostics.length).toBe(1);
592
+ expect(diagnostics[0]?.severity).toBe('warn');
593
+ expect(diagnostics[0]?.message).toContain("'order.placed'");
594
+ expect(diagnostics[0]?.message).toContain('object-form fires entries');
595
+ });
596
+ });
597
+ });
598
+
599
+ describe('fires-declarations helper-scoped blind spots', () => {
600
+ test('helper-scoped ctx.fire stays a documented unused-only blind spot', () => {
601
+ const code = `
602
+ trail('nestedLegit', {
603
+ fires: ['legitimate.signal'],
604
+ blaze: async (input, ctx) => {
605
+ const runLater = () => ctx.fire('legitimate.signal', {});
606
+ runLater();
607
+ return Result.ok({});
608
+ },
609
+ });
610
+ `;
611
+
612
+ const diagnostics = firesDeclarations.check(code, TEST_FILE);
613
+ // Precision tradeoff: helper-scoped ctx.fire isn't walked yet, so the
614
+ // rule sees the declared signal as "unused" today. What matters is that
615
+ // it is NOT reported as undeclared, and that the limitation stays
616
+ // stable until helper-aware analysis lands.
617
+ const undeclared = diagnostics.filter((d) =>
618
+ d.message.includes('not declared in fires')
619
+ );
620
+ const unused = diagnostics.filter((d) =>
621
+ d.message.includes("'legitimate.signal' declared in fires")
622
+ );
623
+ expect(undeclared.length).toBe(0);
624
+ expect(unused.length).toBe(1);
625
+ });
626
+
627
+ test('helper-local destructured fire stays a documented unused-only blind spot', () => {
628
+ const code = `
629
+ trail('nestedDestructure', {
630
+ fires: ['helper.signal'],
631
+ blaze: async (input, ctx) => {
632
+ const runLater = () => {
633
+ const { fire } = ctx;
634
+ return fire('helper.signal', {});
635
+ };
636
+ runLater();
637
+ return Result.ok({});
638
+ },
639
+ });
640
+ `;
641
+
642
+ const diagnostics = firesDeclarations.check(code, TEST_FILE);
643
+ const undeclared = diagnostics.filter((d) =>
644
+ d.message.includes('not declared in fires')
645
+ );
646
+ const unused = diagnostics.filter((d) =>
647
+ d.message.includes("'helper.signal' declared in fires")
648
+ );
649
+ expect(undeclared.length).toBe(0);
650
+ expect(unused.length).toBe(1);
651
+ });
652
+ });
@@ -67,7 +67,7 @@ describe('formatGitHubAnnotations', () => {
67
67
 
68
68
  test('emits drift as a single ::error annotation', () => {
69
69
  const output = formatGitHubAnnotations(reportWithDrift);
70
- expect(output).toContain('::error::drift: trailhead.lock is stale');
70
+ expect(output).toContain('::error::drift: trails.lock is stale');
71
71
  });
72
72
 
73
73
  test('produces one line per diagnostic', () => {
@@ -148,7 +148,7 @@ describe('formatSummary', () => {
148
148
  test('includes drift section when stale', () => {
149
149
  const output = formatSummary(reportWithDrift);
150
150
  expect(output).toContain('### Drift');
151
- expect(output).toContain('trailhead.lock is stale');
151
+ expect(output).toContain('trails.lock is stale');
152
152
  });
153
153
 
154
154
  test('omits drift section when clean', () => {