@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,375 @@
1
+ import {
2
+ AlreadyExistsError,
3
+ AmbiguousError,
4
+ AssertionError,
5
+ AuthError,
6
+ CancelledError,
7
+ ConflictError,
8
+ DerivationError,
9
+ InternalError,
10
+ NetworkError,
11
+ NotFoundError,
12
+ PermissionError,
13
+ RateLimitError,
14
+ RetryExhaustedError,
15
+ TimeoutError,
16
+ TrailsError,
17
+ ValidationError,
18
+ } from '@ontrails/core';
19
+
20
+ import {
21
+ extractStringLiteral,
22
+ findConfigProperty,
23
+ findTrailDefinitions,
24
+ identifierName,
25
+ offsetToLine,
26
+ parse,
27
+ walk,
28
+ } from './ast.js';
29
+ import type { AstNode } from './ast.js';
30
+ import { isTestFile } from './scan.js';
31
+ import type { WardenDiagnostic, WardenRule } from './types.js';
32
+
33
+ interface ErrorTypeShape {
34
+ readonly name: string;
35
+ readonly prototype: TrailsError;
36
+ }
37
+
38
+ interface DetourOnType {
39
+ readonly line: number;
40
+ readonly onType: string;
41
+ }
42
+
43
+ const knownErrorConstructors = new Map<string, ErrorTypeShape>([
44
+ [TrailsError.name, TrailsError],
45
+ [ValidationError.name, ValidationError],
46
+ [AmbiguousError.name, AmbiguousError],
47
+ [AssertionError.name, AssertionError],
48
+ [NotFoundError.name, NotFoundError],
49
+ [AlreadyExistsError.name, AlreadyExistsError],
50
+ [ConflictError.name, ConflictError],
51
+ [PermissionError.name, PermissionError],
52
+ [TimeoutError.name, TimeoutError],
53
+ [RateLimitError.name, RateLimitError],
54
+ [NetworkError.name, NetworkError],
55
+ [InternalError.name, InternalError],
56
+ [DerivationError.name, DerivationError],
57
+ [AuthError.name, AuthError],
58
+ [CancelledError.name, CancelledError],
59
+ [RetryExhaustedError.name, RetryExhaustedError],
60
+ ]);
61
+
62
+ const knownErrorParents = new Map<string, string | null>(
63
+ [...knownErrorConstructors.entries()].map(([name, ctor]) => {
64
+ const parent = Object.getPrototypeOf(ctor.prototype)?.constructor;
65
+ const parentName =
66
+ typeof parent?.name === 'string' &&
67
+ knownErrorConstructors.has(parent.name)
68
+ ? parent.name
69
+ : null;
70
+ return [name, parentName];
71
+ })
72
+ );
73
+
74
+ const resolveKnownErrorName = (
75
+ name: string,
76
+ aliases: ReadonlyMap<string, string>
77
+ ): string => aliases.get(name) ?? name;
78
+
79
+ const coreImportSource = (node: AstNode): string | null =>
80
+ extractStringLiteral((node as unknown as { source?: AstNode }).source);
81
+
82
+ const collectImportSpecifierAliases = (
83
+ specifiers: readonly AstNode[] | undefined,
84
+ aliases: Map<string, string>
85
+ ): void => {
86
+ for (const specifier of specifiers ?? []) {
87
+ if (specifier.type !== 'ImportSpecifier') {
88
+ continue;
89
+ }
90
+
91
+ const localName = identifierName(
92
+ (specifier as unknown as { local?: AstNode }).local
93
+ );
94
+ const importedName =
95
+ identifierName(
96
+ (specifier as unknown as { imported?: AstNode }).imported
97
+ ) ?? localName;
98
+
99
+ if (localName && importedName && knownErrorConstructors.has(importedName)) {
100
+ aliases.set(localName, importedName);
101
+ }
102
+ }
103
+ };
104
+
105
+ const collectKnownErrorAliases = (
106
+ ast: AstNode
107
+ ): ReadonlyMap<string, string> => {
108
+ const aliases = new Map<string, string>();
109
+
110
+ walk(ast, (node) => {
111
+ if (node.type !== 'ImportDeclaration') {
112
+ return;
113
+ }
114
+
115
+ if (coreImportSource(node) !== '@ontrails/core') {
116
+ return;
117
+ }
118
+
119
+ const { specifiers } = node as unknown as {
120
+ specifiers?: readonly AstNode[];
121
+ };
122
+ collectImportSpecifierAliases(specifiers, aliases);
123
+ });
124
+
125
+ return aliases;
126
+ };
127
+
128
+ const recordLocalErrorParent = (
129
+ parents: Map<string, string>,
130
+ aliases: ReadonlyMap<string, string>,
131
+ className: string | null,
132
+ parentName: string | null
133
+ ): void => {
134
+ if (!className || !parentName) {
135
+ return;
136
+ }
137
+
138
+ parents.set(className, resolveKnownErrorName(parentName, aliases));
139
+ };
140
+
141
+ const collectClassExpressionParent = (
142
+ node: AstNode,
143
+ parents: Map<string, string>,
144
+ aliases: ReadonlyMap<string, string>
145
+ ): void => {
146
+ if (node.type !== 'VariableDeclarator') {
147
+ return;
148
+ }
149
+
150
+ const { init } = node as unknown as { init?: AstNode };
151
+ if (!init || init.type !== 'ClassExpression') {
152
+ return;
153
+ }
154
+
155
+ const className = identifierName((node as unknown as { id?: AstNode }).id);
156
+ const parentName = identifierName(
157
+ (init as unknown as { superClass?: AstNode }).superClass
158
+ );
159
+ recordLocalErrorParent(parents, aliases, className, parentName);
160
+ };
161
+
162
+ const collectLocalErrorParents = (
163
+ ast: AstNode,
164
+ aliases: ReadonlyMap<string, string>
165
+ ): ReadonlyMap<string, string> => {
166
+ const parents = new Map<string, string>();
167
+
168
+ walk(ast, (node) => {
169
+ if (node.type === 'ClassDeclaration') {
170
+ const className = identifierName(
171
+ (node as unknown as { id?: AstNode }).id
172
+ );
173
+ const parentName = identifierName(
174
+ (node as unknown as { superClass?: AstNode }).superClass
175
+ );
176
+ recordLocalErrorParent(parents, aliases, className, parentName);
177
+ return;
178
+ }
179
+
180
+ collectClassExpressionParent(node, parents, aliases);
181
+ });
182
+
183
+ return parents;
184
+ };
185
+
186
+ /**
187
+ * Return the raw AST elements of a trail's `detours` array.
188
+ *
189
+ * @remarks
190
+ * Spread elements (`...baseDetours`) in the `detours` array are intentionally
191
+ * skipped here and by {@link extractDetourOnTypes}. This makes the ordering
192
+ * analysis best-effort for arrays that contain spreads: only literal inline
193
+ * detour object entries are ordering-checked, so spreads can cause both false
194
+ * negatives and false positives depending on where they sit relative to the
195
+ * literal entries.
196
+ */
197
+ const getDetourElements = (config: AstNode): readonly (AstNode | null)[] => {
198
+ const detoursProp = findConfigProperty(config, 'detours');
199
+ if (!detoursProp) {
200
+ return [];
201
+ }
202
+
203
+ const detoursValue = detoursProp.value as AstNode | undefined;
204
+ if (!detoursValue || detoursValue.type !== 'ArrayExpression') {
205
+ return [];
206
+ }
207
+
208
+ const elements = (detoursValue as AstNode)['elements'] as
209
+ | readonly (AstNode | null)[]
210
+ | undefined;
211
+ return elements ?? [];
212
+ };
213
+
214
+ const extractDetourOnTypes = (
215
+ config: AstNode,
216
+ sourceCode: string,
217
+ aliases: ReadonlyMap<string, string>
218
+ ): readonly DetourOnType[] =>
219
+ getDetourElements(config).flatMap((element) => {
220
+ if (!element || element.type !== 'ObjectExpression') {
221
+ return [];
222
+ }
223
+
224
+ const onProp = findConfigProperty(element, 'on');
225
+ const onNode = onProp?.value as AstNode | undefined;
226
+ const onTypeName = identifierName(onNode);
227
+ if (!onNode || !onTypeName) {
228
+ return [];
229
+ }
230
+
231
+ return [
232
+ {
233
+ line: offsetToLine(sourceCode, onNode.start),
234
+ onType: resolveKnownErrorName(onTypeName, aliases),
235
+ },
236
+ ];
237
+ });
238
+
239
+ const nextParentType = (
240
+ errorType: string,
241
+ localParents: ReadonlyMap<string, string>
242
+ ): string | null =>
243
+ localParents.get(errorType) ?? knownErrorParents.get(errorType) ?? null;
244
+
245
+ const isSameOrSubtype = (
246
+ candidate: string,
247
+ ancestor: string,
248
+ localParents: ReadonlyMap<string, string>
249
+ ): boolean => {
250
+ let current: string | null = candidate;
251
+ const seen = new Set<string>();
252
+
253
+ while (current && !seen.has(current)) {
254
+ if (current === ancestor) {
255
+ return true;
256
+ }
257
+
258
+ seen.add(current);
259
+ current = nextParentType(current, localParents);
260
+ }
261
+
262
+ return false;
263
+ };
264
+
265
+ const buildDiagnostic = (
266
+ trailId: string,
267
+ shadowedType: string,
268
+ shadowingType: string,
269
+ filePath: string,
270
+ line: number
271
+ ): WardenDiagnostic => ({
272
+ filePath,
273
+ line,
274
+ message: `Trail "${trailId}" declares detour on "${shadowedType}" after earlier detour on "${shadowingType}". Because "${shadowingType}" matches "${shadowedType}" first, the later detour is unreachable.`,
275
+ rule: 'unreachable-detour-shadowing',
276
+ severity: 'error',
277
+ });
278
+
279
+ const findShadowingDetour = (
280
+ detours: readonly DetourOnType[],
281
+ index: number,
282
+ localParents: ReadonlyMap<string, string>
283
+ ): DetourOnType | null => {
284
+ const detour = detours[index];
285
+ if (!detour) {
286
+ return null;
287
+ }
288
+
289
+ for (let previousIndex = 0; previousIndex < index; previousIndex += 1) {
290
+ const previous = detours[previousIndex];
291
+ if (
292
+ previous &&
293
+ isSameOrSubtype(detour.onType, previous.onType, localParents)
294
+ ) {
295
+ return previous;
296
+ }
297
+ }
298
+
299
+ return null;
300
+ };
301
+
302
+ const buildTrailDiagnostics = (
303
+ trailId: string,
304
+ detours: readonly DetourOnType[],
305
+ filePath: string,
306
+ localParents: ReadonlyMap<string, string>
307
+ ): readonly WardenDiagnostic[] => {
308
+ const diagnostics: WardenDiagnostic[] = [];
309
+
310
+ for (let index = 1; index < detours.length; index += 1) {
311
+ const detour = detours[index];
312
+ const shadowing = findShadowingDetour(detours, index, localParents);
313
+ if (!detour || !shadowing) {
314
+ continue;
315
+ }
316
+
317
+ diagnostics.push(
318
+ buildDiagnostic(
319
+ trailId,
320
+ detour.onType,
321
+ shadowing.onType,
322
+ filePath,
323
+ detour.line
324
+ )
325
+ );
326
+ }
327
+
328
+ return diagnostics;
329
+ };
330
+
331
+ const buildDiagnostics = (
332
+ ast: AstNode,
333
+ sourceCode: string,
334
+ filePath: string
335
+ ): readonly WardenDiagnostic[] => {
336
+ const aliases = collectKnownErrorAliases(ast);
337
+ const localParents = collectLocalErrorParents(ast, aliases);
338
+ const diagnostics: WardenDiagnostic[] = [];
339
+
340
+ for (const definition of findTrailDefinitions(ast)) {
341
+ if (definition.kind !== 'trail') {
342
+ continue;
343
+ }
344
+
345
+ diagnostics.push(
346
+ ...buildTrailDiagnostics(
347
+ definition.id,
348
+ extractDetourOnTypes(definition.config, sourceCode, aliases),
349
+ filePath,
350
+ localParents
351
+ )
352
+ );
353
+ }
354
+
355
+ return diagnostics;
356
+ };
357
+
358
+ export const unreachableDetourShadowing: WardenRule = {
359
+ check(sourceCode: string, filePath: string): readonly WardenDiagnostic[] {
360
+ if (isTestFile(filePath)) {
361
+ return [];
362
+ }
363
+
364
+ const ast = parse(filePath, sourceCode);
365
+ if (!ast) {
366
+ return [];
367
+ }
368
+
369
+ return buildDiagnostics(ast, sourceCode, filePath);
370
+ },
371
+ description:
372
+ 'Detect later detours whose on: error type is already matched by an earlier same or broader detour.',
373
+ name: 'unreachable-detour-shadowing',
374
+ severity: 'error',
375
+ };
@@ -1,13 +1,17 @@
1
+ import {
2
+ extractStringOrTemplateLiteral,
3
+ offsetToLine,
4
+ parse,
5
+ walk,
6
+ } from './ast.js';
7
+ import type { AstNode } from './ast.js';
8
+ import { isTestFile } from './scan.js';
9
+ import { collectTrailIds } from './specs.js';
1
10
  import type {
2
11
  ProjectAwareWardenRule,
3
12
  ProjectContext,
4
13
  WardenDiagnostic,
5
14
  } from './types.js';
6
- import { isTestFile } from './scan.js';
7
- import { collectTrailIds, parseStringLiteral } from './specs.js';
8
- import { captureBalanced, lineNumberAt } from './structure.js';
9
-
10
- const DESCRIBE_PATTERN = /\.describe\s*\(/g;
11
15
 
12
16
  const SEE_PATTERN = /@see\s+([A-Za-z0-9_.-]+)/g;
13
17
 
@@ -16,41 +20,160 @@ interface DescribeRef {
16
20
  readonly ref: string;
17
21
  }
18
22
 
19
- const describeTextAt = (
20
- sourceCode: string,
21
- matchIndex: number
22
- ): string | null => {
23
- const openParen = sourceCode.indexOf('(', matchIndex);
24
- if (openParen === -1) {
23
+ const STRING_LITERAL_ARG_TYPES: ReadonlySet<string> = new Set([
24
+ 'Literal',
25
+ 'StringLiteral',
26
+ 'TemplateLiteral',
27
+ ]);
28
+
29
+ const MEMBER_CALLEE_TYPES: ReadonlySet<string> = new Set([
30
+ 'MemberExpression',
31
+ 'StaticMemberExpression',
32
+ ]);
33
+
34
+ const isDescribeMemberCallee = (callee: AstNode | undefined): boolean => {
35
+ if (!callee || !MEMBER_CALLEE_TYPES.has(callee.type)) {
36
+ return false;
37
+ }
38
+ const prop = (callee as unknown as { property?: AstNode }).property;
39
+ return (
40
+ prop?.type === 'Identifier' &&
41
+ (prop as unknown as { name?: string }).name === 'describe'
42
+ );
43
+ };
44
+
45
+ const hasStringLiteralFirstArg = (node: AstNode): boolean => {
46
+ const args = node['arguments'] as readonly AstNode[] | undefined;
47
+ const firstArg = args?.[0];
48
+ return !!firstArg && STRING_LITERAL_ARG_TYPES.has(firstArg.type);
49
+ };
50
+
51
+ const isDescribeCall = (node: AstNode): boolean => {
52
+ if (node.type !== 'CallExpression') {
53
+ return false;
54
+ }
55
+ if (!isDescribeMemberCallee(node['callee'] as AstNode | undefined)) {
56
+ return false;
57
+ }
58
+ // Narrow to calls whose first argument is a string/template literal.
59
+ // Filters out RxJS-style `.describe(fn)` and other custom APIs whose
60
+ // `.describe()` overloads take non-string arguments. Zod's shape always
61
+ // passes a string literal here.
62
+ return hasStringLiteralFirstArg(node);
63
+ };
64
+
65
+ /**
66
+ * Extract scannable text from a template literal, even when it contains
67
+ * `${...}` expressions. Concatenates the cooked quasi chunks with a NUL
68
+ * sentinel between them — interpolated values are runtime-only and cannot
69
+ * contribute static `@see` tokens, but the surrounding quasi text can. The
70
+ * sentinel prevents phantom tokens that would otherwise appear when a quasi
71
+ * boundary splits the `@see` marker itself (e.g. `\`@s${x}ee missing\``
72
+ * would naively join to `"@seemissing"` and match `@see`).
73
+ *
74
+ * This is intentionally describe-local: the shared
75
+ * {@link extractStringOrTemplateLiteral} helper preserves "plain template
76
+ * literal only" semantics for other rules (e.g. resolving trail/signal IDs)
77
+ * that require a single clean string value. Here we only need to scan for
78
+ * `@see` tokens, so concatenating quasi cooked text is sound.
79
+ *
80
+ * @remarks
81
+ * A quasi's `cooked` value can be `null` in tagged-template positions where
82
+ * the literal contains escape sequences the parser can't decode. `.describe`
83
+ * is a plain method call, not a tagged template, so in practice its quasis
84
+ * always have a `cooked` string today. The `raw` fallback is defensive: if a
85
+ * future refactor wraps `describe(\`...\`)` in a tagged template, we still
86
+ * scan the raw source rather than silently dropping the quasi text and
87
+ * missing an `@see` token.
88
+ */
89
+ const extractQuasiText = (quasi: AstNode): string | null => {
90
+ const { value } = quasi as unknown as {
91
+ value?: { cooked?: unknown; raw?: unknown };
92
+ };
93
+ if (typeof value?.cooked === 'string') {
94
+ return value.cooked;
95
+ }
96
+ if (typeof value?.raw === 'string') {
97
+ return value.raw;
98
+ }
99
+ return null;
100
+ };
101
+
102
+ const extractTemplateLiteralQuasiText = (node: AstNode): string | null => {
103
+ if (node.type !== 'TemplateLiteral') {
25
104
  return null;
26
105
  }
106
+ const quasis = (node['quasis'] as readonly AstNode[] | undefined) ?? [];
107
+ const parts: string[] = [];
108
+ for (const quasi of quasis) {
109
+ const text = extractQuasiText(quasi);
110
+ if (text !== null) {
111
+ parts.push(text);
112
+ }
113
+ }
114
+ // Use a NUL sentinel (not a letter / ref character) so interpolation
115
+ // boundaries cannot silently fuse neighbouring quasis into a phantom
116
+ // `@see <ident>` match. `\u0000` cannot appear inside a valid trail ID,
117
+ // so it safely terminates any partial token on either side.
118
+ return parts.join('\u0000');
119
+ };
27
120
 
28
- return captureBalanced(sourceCode, openParen)?.text.slice(1, -1) ?? null;
121
+ const extractDescribeDescription = (node: AstNode): string | null => {
122
+ const args = node['arguments'] as readonly AstNode[] | undefined;
123
+ const [firstArg] = args ?? [];
124
+ if (!firstArg) {
125
+ return null;
126
+ }
127
+ return (
128
+ extractStringOrTemplateLiteral(firstArg) ??
129
+ extractTemplateLiteralQuasiText(firstArg)
130
+ );
29
131
  };
30
132
 
31
- const refsInDescription = (
133
+ /**
134
+ * Anchor the diagnostic on the string argument that actually contains the
135
+ * `@see` token, not on the call-expression start. For multi-line schema
136
+ * chains, the call-expression start can be many lines above the describe
137
+ * argument, which confuses editor tooling.
138
+ */
139
+ const describeAnchorOffset = (node: AstNode): number => {
140
+ const args = node['arguments'] as readonly AstNode[] | undefined;
141
+ return args?.[0]?.start ?? node.start;
142
+ };
143
+
144
+ const collectRefsFromDescription = (
32
145
  description: string,
33
- line: number
34
- ): readonly DescribeRef[] =>
35
- [...description.matchAll(SEE_PATTERN)].flatMap((see) =>
36
- see[1] ? [{ line, ref: see[1] }] : []
37
- );
146
+ line: number,
147
+ out: DescribeRef[]
148
+ ): void => {
149
+ for (const match of description.matchAll(SEE_PATTERN)) {
150
+ const [, ref] = match;
151
+ if (ref) {
152
+ out.push({ line, ref });
153
+ }
154
+ }
155
+ };
38
156
 
39
- const refsForDescribe = (
40
- sourceCode: string,
41
- matchIndex: number
157
+ const collectDescribeRefs = (
158
+ ast: AstNode,
159
+ sourceCode: string
42
160
  ): readonly DescribeRef[] => {
43
- const args = describeTextAt(sourceCode, matchIndex);
44
- const description = args ? parseStringLiteral(args) : null;
45
- return description === null
46
- ? []
47
- : refsInDescription(description, lineNumberAt(sourceCode, matchIndex));
48
- };
161
+ const refs: DescribeRef[] = [];
49
162
 
50
- const collectDescribeRefs = (sourceCode: string): readonly DescribeRef[] =>
51
- [...sourceCode.matchAll(DESCRIBE_PATTERN)].flatMap((match) =>
52
- match.index === undefined ? [] : refsForDescribe(sourceCode, match.index)
53
- );
163
+ walk(ast, (node) => {
164
+ if (!isDescribeCall(node)) {
165
+ return;
166
+ }
167
+ const description = extractDescribeDescription(node);
168
+ if (description === null) {
169
+ return;
170
+ }
171
+ const line = offsetToLine(sourceCode, describeAnchorOffset(node));
172
+ collectRefsFromDescription(description, line, refs);
173
+ });
174
+
175
+ return refs;
176
+ };
54
177
 
55
178
  const checkDescribeRefs = (
56
179
  sourceCode: string,
@@ -61,7 +184,12 @@ const checkDescribeRefs = (
61
184
  return [];
62
185
  }
63
186
 
64
- return collectDescribeRefs(sourceCode)
187
+ const ast = parse(filePath, sourceCode);
188
+ if (!ast) {
189
+ return [];
190
+ }
191
+
192
+ return collectDescribeRefs(ast, sourceCode)
65
193
  .filter(({ ref }) => !knownTrailIds.has(ref))
66
194
  .map(({ line, ref }) => ({
67
195
  filePath,
@@ -0,0 +1,78 @@
1
+ import type { Topo } from '@ontrails/core';
2
+
3
+ import type { TopoAwareWardenRule, WardenDiagnostic } from './types.js';
4
+
5
+ interface DetourLike {
6
+ readonly on?: unknown;
7
+ readonly recover?: unknown;
8
+ }
9
+
10
+ const isErrorConstructor = (
11
+ value: unknown
12
+ ): value is abstract new (...args: never[]) => Error => {
13
+ if (typeof value !== 'function') {
14
+ return false;
15
+ }
16
+
17
+ const { prototype } = value as { prototype?: unknown };
18
+ return prototype instanceof Error;
19
+ };
20
+
21
+ const describeOnValue = (value: unknown): string => {
22
+ if (typeof value === 'function') {
23
+ const { name } = value as { name?: unknown };
24
+ return typeof name === 'string' && name.length > 0
25
+ ? name
26
+ : '<anonymous constructor>';
27
+ }
28
+
29
+ return String(value);
30
+ };
31
+
32
+ const buildDiagnostic = (message: string, rule: string): WardenDiagnostic => ({
33
+ filePath: '<topo>',
34
+ line: 1,
35
+ message,
36
+ rule,
37
+ severity: 'error',
38
+ });
39
+
40
+ const collectTrailDiagnostics = (topo: Topo): readonly WardenDiagnostic[] => {
41
+ const diagnostics: WardenDiagnostic[] = [];
42
+
43
+ for (const trail of topo.trails.values()) {
44
+ for (const [index, detour] of trail.detours.entries()) {
45
+ const candidate = detour as DetourLike;
46
+
47
+ if (!isErrorConstructor(candidate.on)) {
48
+ diagnostics.push(
49
+ buildDiagnostic(
50
+ `Trail "${trail.id}" detour[${index}] must declare an error constructor in on:. Received ${describeOnValue(candidate.on)}.`,
51
+ 'valid-detour-contract'
52
+ )
53
+ );
54
+ }
55
+
56
+ if (typeof candidate.recover !== 'function') {
57
+ diagnostics.push(
58
+ buildDiagnostic(
59
+ `Trail "${trail.id}" detour[${index}] must declare a callable recover function.`,
60
+ 'valid-detour-contract'
61
+ )
62
+ );
63
+ }
64
+ }
65
+ }
66
+
67
+ return diagnostics;
68
+ };
69
+
70
+ export const validDetourContract: TopoAwareWardenRule = {
71
+ checkTopo(topo: Topo): readonly WardenDiagnostic[] {
72
+ return collectTrailDiagnostics(topo);
73
+ },
74
+ description:
75
+ 'Ensure detours use real error constructors and callable recover functions.',
76
+ name: 'valid-detour-contract',
77
+ severity: 'error',
78
+ };