@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,613 @@
1
+ import { describe, expect, test } from 'bun:test';
2
+
3
+ import {
4
+ __collectFrameworkNamespaceBindingsForTest,
5
+ collectContourDefinitionIds,
6
+ collectContourReferenceSites,
7
+ collectNamedContourIds,
8
+ __getTrailCalleeNameForTest,
9
+ deriveContourIdentifierName,
10
+ findContourDefinitions,
11
+ findTrailDefinitions,
12
+ hasIgnoreCommentOnLine,
13
+ parse,
14
+ splitSourceLines,
15
+ } from '../rules/ast.js';
16
+
17
+ describe('deriveContourIdentifierName', () => {
18
+ test('supports the common *Contour binding suffix when resolving known contours', () => {
19
+ expect(
20
+ deriveContourIdentifierName(
21
+ 'userContour',
22
+ new Map<string, string>(),
23
+ new Set(['user'])
24
+ )
25
+ ).toBe('user');
26
+ });
27
+
28
+ test('prefers exact contour ids over the *Contour fallback', () => {
29
+ expect(
30
+ deriveContourIdentifierName(
31
+ 'userContour',
32
+ new Map<string, string>(),
33
+ new Set(['user', 'userContour'])
34
+ )
35
+ ).toBe('userContour');
36
+ });
37
+ });
38
+
39
+ describe('hasIgnoreCommentOnLine', () => {
40
+ test('matches the pragma when the preceding line is exact', () => {
41
+ const lines = splitSourceLines(
42
+ "// warden-ignore-next-line\nconst x = '_draft.foo';\n"
43
+ );
44
+ expect(hasIgnoreCommentOnLine(lines, 2)).toBe(true);
45
+ });
46
+
47
+ test('matches the pragma with leading whitespace', () => {
48
+ const lines = splitSourceLines(
49
+ " // warden-ignore-next-line\n const x = '_draft.foo';\n"
50
+ );
51
+ expect(hasIgnoreCommentOnLine(lines, 2)).toBe(true);
52
+ });
53
+
54
+ test('matches the pragma with trailing whitespace (editor did not auto-trim)', () => {
55
+ const lines = splitSourceLines(
56
+ "// warden-ignore-next-line \nconst x = '_draft.foo';\n"
57
+ );
58
+ expect(hasIgnoreCommentOnLine(lines, 2)).toBe(true);
59
+ });
60
+
61
+ test('returns false when there is no preceding line', () => {
62
+ const lines = splitSourceLines("const x = '_draft.foo';\n");
63
+ expect(hasIgnoreCommentOnLine(lines, 1)).toBe(false);
64
+ });
65
+
66
+ test('returns false when the preceding line is blank', () => {
67
+ const lines = splitSourceLines(
68
+ "// warden-ignore-next-line\n\nconst x = '_draft.foo';\n"
69
+ );
70
+ expect(hasIgnoreCommentOnLine(lines, 3)).toBe(false);
71
+ });
72
+
73
+ test('accepts pre-split lines so callers memoize across many matches', () => {
74
+ // Regression guard for the O(N × source length) re-split fix. The caller
75
+ // splits once and threads the same lines array through to every lookup.
76
+ const source = Array.from(
77
+ { length: 100 },
78
+ (_, i) => `const v${i} = '_draft.id${i}';`
79
+ ).join('\n');
80
+ const lines = splitSourceLines(source);
81
+ for (let line = 1; line <= 100; line += 1) {
82
+ expect(hasIgnoreCommentOnLine(lines, line)).toBe(false);
83
+ }
84
+ });
85
+ });
86
+
87
+ const parseOrThrow = (source: string) =>
88
+ parse('test.ts', source) ??
89
+ (() => {
90
+ throw new Error('failed to parse');
91
+ })();
92
+
93
+ const parseCallee = (source: string) => {
94
+ const ast = parseOrThrow(source);
95
+ // The first statement is an ExpressionStatement wrapping the CallExpression.
96
+ const [stmt] = (ast as unknown as { body: readonly unknown[] }).body;
97
+ const { expression } = stmt as { expression: unknown };
98
+ return expression as Parameters<typeof __getTrailCalleeNameForTest>[0];
99
+ };
100
+
101
+ const coreNamespaces: ReadonlySet<string> = new Set(['core']);
102
+
103
+ describe('getTrailCalleeName', () => {
104
+ test('matches bare trail(...) identifier callees', () => {
105
+ expect(__getTrailCalleeNameForTest(parseCallee('trail("foo", {});'))).toBe(
106
+ 'trail'
107
+ );
108
+ });
109
+
110
+ test('matches namespaced ns.trail(...) callees when the namespace is from @ontrails/*', () => {
111
+ expect(
112
+ __getTrailCalleeNameForTest(
113
+ parseCallee('core.trail("foo", {});'),
114
+ coreNamespaces
115
+ )
116
+ ).toBe('trail');
117
+ });
118
+
119
+ test('matches bare signal(...) identifier callees', () => {
120
+ expect(__getTrailCalleeNameForTest(parseCallee('signal("evt", {});'))).toBe(
121
+ 'signal'
122
+ );
123
+ });
124
+
125
+ test('matches namespaced ns.signal(...) callees when the namespace is from @ontrails/*', () => {
126
+ expect(
127
+ __getTrailCalleeNameForTest(
128
+ parseCallee('core.signal("evt", {});'),
129
+ coreNamespaces
130
+ )
131
+ ).toBe('signal');
132
+ });
133
+
134
+ test('rejects computed member access like ns[trail](...)', () => {
135
+ expect(
136
+ __getTrailCalleeNameForTest(
137
+ parseCallee('ns[trail]("foo", {});'),
138
+ new Set(['ns'])
139
+ )
140
+ ).toBeNull();
141
+ });
142
+
143
+ test('rejects unrelated bare callees', () => {
144
+ expect(
145
+ __getTrailCalleeNameForTest(parseCallee('other("foo", {});'))
146
+ ).toBeNull();
147
+ });
148
+
149
+ test('rejects unrelated namespaced callees', () => {
150
+ expect(
151
+ __getTrailCalleeNameForTest(
152
+ parseCallee('ns.other("foo", {});'),
153
+ new Set(['ns'])
154
+ )
155
+ ).toBeNull();
156
+ });
157
+
158
+ test('rejects namespaced callees when the receiver is not a framework namespace', () => {
159
+ // `analytics.trail(...)` must not be mistaken for `core.trail(...)` when
160
+ // the `analytics` binding is not an `@ontrails/*` namespace import.
161
+ expect(
162
+ __getTrailCalleeNameForTest(
163
+ parseCallee('analytics.trail("foo", {});'),
164
+ coreNamespaces
165
+ )
166
+ ).toBeNull();
167
+ });
168
+ });
169
+
170
+ describe('collectFrameworkNamespaceBindings', () => {
171
+ test('collects the local name of an @ontrails/core namespace import', () => {
172
+ const ast = parseOrThrow(`
173
+ import * as core from '@ontrails/core';
174
+ `);
175
+ expect(
176
+ [...__collectFrameworkNamespaceBindingsForTest(ast)].toSorted()
177
+ ).toEqual(['core']);
178
+ });
179
+
180
+ test('collects bindings for any @ontrails/* scoped package', () => {
181
+ const ast = parseOrThrow(`
182
+ import * as core from '@ontrails/core';
183
+ import * as warden from '@ontrails/warden';
184
+ `);
185
+ expect(
186
+ [...__collectFrameworkNamespaceBindingsForTest(ast)].toSorted()
187
+ ).toEqual(['core', 'warden']);
188
+ });
189
+
190
+ test('ignores namespace imports from non-framework packages', () => {
191
+ const ast = parseOrThrow(`
192
+ import * as analytics from 'analytics';
193
+ `);
194
+ expect([...__collectFrameworkNamespaceBindingsForTest(ast)]).toEqual([]);
195
+ });
196
+
197
+ test('ignores named imports from @ontrails/* packages', () => {
198
+ const ast = parseOrThrow(`
199
+ import { trail } from '@ontrails/core';
200
+ `);
201
+ expect([...__collectFrameworkNamespaceBindingsForTest(ast)]).toEqual([]);
202
+ });
203
+ });
204
+
205
+ describe('findTrailDefinitions with namespaced callees', () => {
206
+ test('discovers core.trail("id", { ... }) definitions', () => {
207
+ const source = `
208
+ import * as core from '@ontrails/core';
209
+ export const t = core.trail('entity.show', {
210
+ input: {},
211
+ });
212
+ `;
213
+ const ast = parseOrThrow(source);
214
+ const defs = findTrailDefinitions(ast);
215
+ expect(defs).toHaveLength(1);
216
+ expect(defs[0]?.id).toBe('entity.show');
217
+ expect(defs[0]?.kind).toBe('trail');
218
+ });
219
+
220
+ test('discovers core.trail({ id: "x", ... }) single-object form', () => {
221
+ // Regression: confirms the single-object form (`trail({ id: 'x', ... })`)
222
+ // is discovered the same way as the two-arg form via a namespaced callee.
223
+ const source = `
224
+ import * as core from '@ontrails/core';
225
+ export const t = core.trail({ id: 'entity.show', input: {} });
226
+ `;
227
+ const ast = parseOrThrow(source);
228
+ const defs = findTrailDefinitions(ast);
229
+ expect(defs).toHaveLength(1);
230
+ expect(defs[0]?.id).toBe('entity.show');
231
+ expect(defs[0]?.kind).toBe('trail');
232
+ });
233
+
234
+ test('discovers core.signal("id", { ... }) definitions', () => {
235
+ const source = `
236
+ import * as core from '@ontrails/core';
237
+ export const s = core.signal('entity.created', { payload: {} });
238
+ `;
239
+ const ast = parseOrThrow(source);
240
+ const defs = findTrailDefinitions(ast);
241
+ expect(defs).toHaveLength(1);
242
+ expect(defs[0]?.id).toBe('entity.created');
243
+ expect(defs[0]?.kind).toBe('signal');
244
+ });
245
+
246
+ test('still ignores computed-member access ns[trail]("id", ...)', () => {
247
+ const source = `
248
+ const trail = 'x';
249
+ ns[trail]('entity.show', { input: {} });
250
+ `;
251
+ const ast = parseOrThrow(source);
252
+ expect(findTrailDefinitions(ast)).toHaveLength(0);
253
+ });
254
+
255
+ test('ignores unrelated ns.trail(...) where ns is not an @ontrails import', () => {
256
+ // Regression: `analytics.trail(...)` where `analytics` is not a framework
257
+ // namespace must not be picked up as a trail definition.
258
+ const source = `
259
+ import * as analytics from 'analytics';
260
+ analytics.trail('entity.show', { input: {} });
261
+ `;
262
+ const ast = parseOrThrow(source);
263
+ expect(findTrailDefinitions(ast)).toHaveLength(0);
264
+ });
265
+ });
266
+
267
+ describe('findTrailDefinitions scope-aware shadowing', () => {
268
+ test('ignores core.trail(...) inside a function that locally shadows the namespace', () => {
269
+ // Regression: a function-local `const core = {...}` must shadow the
270
+ // module-level `import * as core from '@ontrails/core'` for the duration
271
+ // of that function. A name-only check would let the local `core.trail()`
272
+ // through; scope-aware resolution rejects it.
273
+ const source = `
274
+ import * as core from '@ontrails/core';
275
+ function weird() {
276
+ const core = { trail: (_id: string, _cfg: object) => undefined };
277
+ core.trail('entity.show', { input: {} });
278
+ }
279
+ `;
280
+ const ast = parseOrThrow(source);
281
+ expect(findTrailDefinitions(ast)).toHaveLength(0);
282
+ });
283
+
284
+ test('still discovers module-level core.trail(...) when a sibling function shadows the namespace', () => {
285
+ // Sanity check: a shadow inside one function must not suppress a
286
+ // legitimate `core.trail(...)` at module scope.
287
+ const source = `
288
+ import * as core from '@ontrails/core';
289
+ function weird() {
290
+ const core = { trail: (_id: string, _cfg: object) => undefined };
291
+ core.trail('entity.local', { input: {} });
292
+ }
293
+ export const t = core.trail('entity.show', { input: {} });
294
+ `;
295
+ const ast = parseOrThrow(source);
296
+ const defs = findTrailDefinitions(ast);
297
+ expect(defs).toHaveLength(1);
298
+ expect(defs[0]?.id).toBe('entity.show');
299
+ });
300
+
301
+ test('ignores core.trail(...) when a function parameter shadows the namespace', () => {
302
+ const source = `
303
+ import * as core from '@ontrails/core';
304
+ function weird(core: { trail: (id: string, cfg: object) => void }) {
305
+ core.trail('entity.show', { input: {} });
306
+ }
307
+ `;
308
+ const ast = parseOrThrow(source);
309
+ expect(findTrailDefinitions(ast)).toHaveLength(0);
310
+ });
311
+
312
+ test('ignores core.trail(...) when a ClassDeclaration shadows the namespace', () => {
313
+ // A class declaration binds its name in the enclosing scope. Inside the
314
+ // class body the name refers to the class, shadowing any module-level
315
+ // namespace import of the same name.
316
+ const source = `
317
+ import * as core from '@ontrails/core';
318
+ class core {
319
+ field = core.trail('entity.show', { input: {} });
320
+ }
321
+ `;
322
+ const ast = parseOrThrow(source);
323
+ expect(findTrailDefinitions(ast)).toHaveLength(0);
324
+ });
325
+
326
+ test('ignores core.trail(...) when a named ClassExpression shadows the namespace inside its body', () => {
327
+ // A named class expression's name is visible only inside its own body.
328
+ const source = `
329
+ import * as core from '@ontrails/core';
330
+ const C = class core {
331
+ field = core.trail('entity.show', { input: {} });
332
+ };
333
+ `;
334
+ const ast = parseOrThrow(source);
335
+ expect(findTrailDefinitions(ast)).toHaveLength(0);
336
+ });
337
+
338
+ test('ignores core.trail(...) when a TSEnumDeclaration shadows the namespace', () => {
339
+ const source = `
340
+ import * as core from '@ontrails/core';
341
+ enum core { A, B }
342
+ core.trail('entity.show', { input: {} });
343
+ `;
344
+ const ast = parseOrThrow(source);
345
+ expect(findTrailDefinitions(ast)).toHaveLength(0);
346
+ });
347
+
348
+ test('ignores core.trail(...) when a TSModuleDeclaration (namespace) shadows the namespace', () => {
349
+ const source = `
350
+ import * as core from '@ontrails/core';
351
+ namespace core { export const x = 1; }
352
+ core.trail('entity.show', { input: {} });
353
+ `;
354
+ const ast = parseOrThrow(source);
355
+ expect(findTrailDefinitions(ast)).toHaveLength(0);
356
+ });
357
+
358
+ test('ignores core.trail(...) inside a FunctionExpression body that locally shadows the namespace', () => {
359
+ // oxc-parser emits `FunctionBody` for `function` expression bodies, not
360
+ // `BlockStatement`. Without a `FunctionBody` entry in the scope-frame
361
+ // collectors, a local `const core = {...}` at the top of the expression
362
+ // body would not push a frame and the shadow would be missed.
363
+ const source = `
364
+ import * as core from '@ontrails/core';
365
+ const fn = function weird() {
366
+ const core = { trail: (_id: string, _cfg: object) => undefined };
367
+ core.trail('entity.show', { input: {} });
368
+ };
369
+ `;
370
+ const ast = parseOrThrow(source);
371
+ expect(findTrailDefinitions(ast)).toHaveLength(0);
372
+ });
373
+
374
+ test('does not hoist block-local function declarations out of their block', () => {
375
+ // A `function core(){}` inside an `if` block is block-scoped in strict
376
+ // (module) code. Hoisting it to the enclosing function frame would
377
+ // wrongly shadow the module-level `core` namespace for later code in
378
+ // the same function, dropping the trail detection below.
379
+ const source = `
380
+ import * as core from '@ontrails/core';
381
+ export function outer() {
382
+ if (Math.random() > 0) {
383
+ function core() { return 0; }
384
+ core();
385
+ }
386
+ return core.trail('entity.show', { input: {} });
387
+ }
388
+ `;
389
+ const ast = parseOrThrow(source);
390
+ const defs = findTrailDefinitions(ast);
391
+ expect(defs).toHaveLength(1);
392
+ expect(defs[0]?.id).toBe('entity.show');
393
+ });
394
+ });
395
+
396
+ describe('getTrailCalleeName permissive fallback', () => {
397
+ test('resolves namespaced core.trail(...) when no context is provided', () => {
398
+ // Inline resolution paths (`crosses: [core.trail(...)]`,
399
+ // `on: [core.signal(...)]`) do not have access to the surrounding file
400
+ // AST and so cannot build a FrameworkNamespaceContext. They must still
401
+ // be able to recognize the trail/signal primitive by name.
402
+ expect(
403
+ __getTrailCalleeNameForTest(parseCallee('core.trail("foo", {});'))
404
+ ).toBe('trail');
405
+ expect(
406
+ __getTrailCalleeNameForTest(parseCallee('core.signal("evt", {});'))
407
+ ).toBe('signal');
408
+ });
409
+ });
410
+
411
+ describe('findContourDefinitions with namespaced callees', () => {
412
+ test('discovers core.contour("name", { ... }) definitions', () => {
413
+ const source = `
414
+ import * as core from '@ontrails/core';
415
+ export const user = core.contour('user', { id: 'string' });
416
+ `;
417
+ const ast = parseOrThrow(source);
418
+ const defs = findContourDefinitions(ast);
419
+ expect(defs).toHaveLength(1);
420
+ expect(defs[0]?.name).toBe('user');
421
+ });
422
+
423
+ test('ignores unrelated ns.contour(...) where ns is not @ontrails/*', () => {
424
+ const source = `
425
+ import * as analytics from 'analytics';
426
+ analytics.contour('user', { id: 'string' });
427
+ `;
428
+ const ast = parseOrThrow(source);
429
+ expect(findContourDefinitions(ast)).toHaveLength(0);
430
+ });
431
+
432
+ test('still rejects computed member access', () => {
433
+ const source = `
434
+ const contour = 'x';
435
+ ns[contour]('user', { id: 'string' });
436
+ `;
437
+ const ast = parseOrThrow(source);
438
+ expect(findContourDefinitions(ast)).toHaveLength(0);
439
+ });
440
+ });
441
+
442
+ describe('findContourDefinitions inline discovery', () => {
443
+ // Regression: `findContourDefinitions` descends into nested object
444
+ // expressions and surfaces inline `core.contour('inner', ...)` calls as
445
+ // definitions alongside the outer binding. This behavior is load-bearing for
446
+ // reference-site resolution (see `collectContourReferenceSites`) and must
447
+ // not silently regress.
448
+ const inlineSource = `
449
+ import * as core from '@ontrails/core';
450
+ import { z } from 'zod';
451
+
452
+ export const outer = core.contour('outer', {
453
+ id: z.string().uuid(),
454
+ inner: core.contour('inner', { id: z.string().uuid() }).id(),
455
+ });
456
+ `;
457
+
458
+ test('returns both outer and inline contour definitions by default', () => {
459
+ const ast = parseOrThrow(inlineSource);
460
+ const defs = findContourDefinitions(ast);
461
+
462
+ expect(defs).toHaveLength(2);
463
+ const names = defs.map((d) => d.name).toSorted();
464
+ expect(names).toEqual(['inner', 'outer']);
465
+
466
+ const outer = defs.find((d) => d.name === 'outer');
467
+ const inner = defs.find((d) => d.name === 'inner');
468
+ expect(outer?.bindingName).toBe('outer');
469
+ // Inline contours are anonymous call expressions — no binding name.
470
+ expect(inner?.bindingName).toBeUndefined();
471
+ });
472
+
473
+ test('collectContourDefinitionIds includes inline contour ids', () => {
474
+ const ast = parseOrThrow(inlineSource);
475
+ const ids = collectContourDefinitionIds(ast);
476
+
477
+ expect(ids.has('outer')).toBe(true);
478
+ expect(ids.has('inner')).toBe(true);
479
+ });
480
+
481
+ test('collectNamedContourIds excludes inline contours (no bindingName)', () => {
482
+ const ast = parseOrThrow(inlineSource);
483
+ const named = collectNamedContourIds(ast);
484
+
485
+ expect([...named.keys()].toSorted()).toEqual(['outer']);
486
+ expect(named.get('outer')).toBe('outer');
487
+ expect(named.has('inner')).toBe(false);
488
+ });
489
+
490
+ test('topLevelOnly: true skips inline contour discovery', () => {
491
+ const ast = parseOrThrow(inlineSource);
492
+ const defs = findContourDefinitions(ast, undefined, {
493
+ topLevelOnly: true,
494
+ });
495
+
496
+ expect(defs).toHaveLength(1);
497
+ expect(defs[0]?.name).toBe('outer');
498
+ expect(defs[0]?.bindingName).toBe('outer');
499
+ });
500
+
501
+ test('topLevelOnly: true still surfaces top-level statement-form calls', () => {
502
+ // Regression for Codex P2 on PR #222: the `topLevelOnly` guard must only
503
+ // exclude inline nested contour calls. Top-level bare-statement forms
504
+ // (`core.contour('name', {...});` directly in the program body, not
505
+ // bound to a variable) are top-level and should still be returned.
506
+ const statementFormSource = `
507
+ import * as core from '@ontrails/core';
508
+ import { z } from 'zod';
509
+
510
+ export const bound = core.contour('bound', {
511
+ id: z.string().uuid(),
512
+ });
513
+
514
+ core.contour('bare', { id: z.string().uuid() });
515
+ `;
516
+ const ast = parseOrThrow(statementFormSource);
517
+ const defs = findContourDefinitions(ast, undefined, {
518
+ topLevelOnly: true,
519
+ });
520
+
521
+ const names = defs.map((d) => d.name).toSorted();
522
+ expect(names).toEqual(['bare', 'bound']);
523
+
524
+ const bound = defs.find((d) => d.name === 'bound');
525
+ const bare = defs.find((d) => d.name === 'bare');
526
+ expect(bound?.bindingName).toBe('bound');
527
+ // Bare statement-form calls have no local binding.
528
+ expect(bare?.bindingName).toBeUndefined();
529
+ });
530
+
531
+ test('topLevelOnly: true surfaces export default core.contour(...) form', () => {
532
+ // Regression for Greptile P2 on PR #227: collectTopLevelStatementCallStarts
533
+ // branches on ExportDefaultDeclaration via getCandidateCallHosts, so an
534
+ // export-default contour declaration must still be surfaced under the
535
+ // topLevelOnly: true flag.
536
+ const exportDefaultSource = `
537
+ import * as core from '@ontrails/core';
538
+ import { z } from 'zod';
539
+
540
+ export default core.contour('default-export', {
541
+ id: z.string().uuid(),
542
+ });
543
+ `;
544
+ const ast = parseOrThrow(exportDefaultSource);
545
+ const defs = findContourDefinitions(ast, undefined, {
546
+ topLevelOnly: true,
547
+ });
548
+
549
+ expect(defs).toHaveLength(1);
550
+ expect(defs[0]?.name).toBe('default-export');
551
+ // Export-default call expressions have no local binding name.
552
+ expect(defs[0]?.bindingName).toBeUndefined();
553
+ });
554
+ });
555
+
556
+ describe('collectContourReferenceSites with namespaced inline contours', () => {
557
+ test('resolves core.contour(...).id() when the file context is available', () => {
558
+ const source = `
559
+ import * as core from '@ontrails/core';
560
+ import { z } from 'zod';
561
+
562
+ const gist = core.contour('gist', {
563
+ id: z.string().uuid(),
564
+ ownerId: core.contour('user', { id: z.string().uuid() }).id(),
565
+ });
566
+ `;
567
+ const ast = parseOrThrow(source);
568
+ const refs = collectContourReferenceSites(ast);
569
+
570
+ expect(refs).toHaveLength(1);
571
+ expect(refs[0]?.source).toBe('gist');
572
+ expect(refs[0]?.field).toBe('ownerId');
573
+ expect(refs[0]?.target).toBe('user');
574
+ });
575
+
576
+ test('ignores analytics.contour(...).id() when the receiver is not a framework namespace', () => {
577
+ const source = `
578
+ import * as analytics from 'analytics';
579
+ import { z } from 'zod';
580
+
581
+ const gist = contour('gist', {
582
+ id: z.string().uuid(),
583
+ ownerId: analytics.contour('user', { id: z.string().uuid() }).id(),
584
+ });
585
+ `;
586
+ const ast = parseOrThrow(source);
587
+
588
+ expect(collectContourReferenceSites(ast)).toEqual([]);
589
+ });
590
+
591
+ test('unwraps wrapped contour id schemas before resolving the target', () => {
592
+ const source = `
593
+ import { contour } from '@ontrails/core';
594
+ import { z } from 'zod';
595
+
596
+ const user = contour('user', {
597
+ id: z.string().uuid(),
598
+ });
599
+
600
+ const gist = contour('gist', {
601
+ id: z.string().uuid(),
602
+ ownerId: user.id().nullable().optional().default(null),
603
+ });
604
+ `;
605
+ const ast = parseOrThrow(source);
606
+ const refs = collectContourReferenceSites(ast);
607
+
608
+ expect(refs).toHaveLength(1);
609
+ expect(refs[0]?.source).toBe('gist');
610
+ expect(refs[0]?.field).toBe('ownerId');
611
+ expect(refs[0]?.target).toBe('user');
612
+ });
613
+ });