@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
package/dist/rules/ast.js CHANGED
@@ -4,7 +4,11 @@
4
4
  * Uses oxc-parser for native-speed TypeScript parsing. Provides a lightweight
5
5
  * walker and helpers for finding trail implementation bodies.
6
6
  */
7
+ import { resolve } from 'node:path';
8
+ import { fileURLToPath } from 'node:url';
9
+ import { DRAFT_ID_PREFIX } from '@ontrails/core';
7
10
  import { parseSync } from 'oxc-parser';
11
+ const isAstNode = (value) => Boolean(value && typeof value === 'object' && value.type);
8
12
  // ---------------------------------------------------------------------------
9
13
  // Parser
10
14
  // ---------------------------------------------------------------------------
@@ -62,7 +66,7 @@ const walkScopeInner = (node, visit) => {
62
66
  /**
63
67
  * Walk an AST node tree without descending into nested function scopes.
64
68
  * The root node is always traversed; only inner function boundaries are skipped.
65
- * Useful for provision-access analysis where inner functions may shadow
69
+ * Useful for resource-access analysis where inner functions may shadow
66
70
  * the trail context parameter name.
67
71
  */
68
72
  export const walkScope = (node, visit) => {
@@ -113,8 +117,181 @@ export const getStringValue = (node) => {
113
117
  const val = node.value;
114
118
  return typeof val === 'string' ? val : null;
115
119
  };
120
+ /**
121
+ * Best-effort resolution of `const NAME = 'value'` declarations via regex.
122
+ *
123
+ * Returns the string value if a simple `const <name> = '...'` or `"..."` is
124
+ * found in the source. Returns null for anything more complex. Shared between
125
+ * warden rules that need to resolve identifier references to signal / trail
126
+ * IDs at lint time.
127
+ */
128
+ export const deriveConstString = (name, sourceCode) => {
129
+ const pattern = new RegExp(`const\\s+${name}\\s*=\\s*(?:'([^']*)'|"([^"]*)")`);
130
+ const match = pattern.exec(sourceCode);
131
+ if (!match) {
132
+ return null;
133
+ }
134
+ return match[1] ?? match[2] ?? null;
135
+ };
116
136
  /** Extract a string literal value, or null when the node is not a string. */
117
137
  export const extractStringLiteral = (node) => node && isStringLiteral(node) ? getStringValue(node) : null;
138
+ /**
139
+ * Extract the cooked value from a `TemplateLiteral` with no interpolations
140
+ * (e.g. `` `entity.fallback` ``). Template literals with `${...}` expressions
141
+ * cannot be resolved at lint time and return null.
142
+ *
143
+ * Shared helper used by rules that accept both string literals and simple
144
+ * backtick-literal IDs (e.g. `valid-describe-refs`).
145
+ */
146
+ const getSingleQuasi = (node) => {
147
+ const expressions = node['expressions'] ?? [];
148
+ if (expressions.length > 0) {
149
+ return null;
150
+ }
151
+ const quasis = node['quasis'] ?? [];
152
+ return quasis.length === 1 ? (quasis[0] ?? null) : null;
153
+ };
154
+ export const extractPlainTemplateLiteral = (node) => {
155
+ if (!node || node.type !== 'TemplateLiteral') {
156
+ return null;
157
+ }
158
+ const quasi = getSingleQuasi(node);
159
+ if (!quasi) {
160
+ return null;
161
+ }
162
+ const cooked = quasi.value
163
+ ?.cooked;
164
+ return typeof cooked === 'string' ? cooked : null;
165
+ };
166
+ /**
167
+ * Extract a string value from either a string literal or a plain template
168
+ * literal (no `${...}` expressions). Returns null for anything else.
169
+ */
170
+ export const extractStringOrTemplateLiteral = (node) => extractStringLiteral(node) ?? extractPlainTemplateLiteral(node);
171
+ /**
172
+ * Names of framework constants whose value is a draft-marker prefix literal.
173
+ *
174
+ * String literals that initialize a `const` declaration with one of these
175
+ * names are treated as the framework's own draft-marker declarations, not as
176
+ * draft-id usage. This list is intentionally small and explicit — adding a
177
+ * new framework draft-prefix constant requires updating this set.
178
+ */
179
+ export const FRAMEWORK_DRAFT_PREFIX_CONSTANT_NAMES = new Set(['DRAFT_ID_PREFIX', 'DRAFT_FILE_PREFIX']);
180
+ /**
181
+ * Exact string literal value allowed for framework draft-prefix constant
182
+ * declarations. Tightens the exemption so a future framework file cannot
183
+ * redeclare `DRAFT_ID_PREFIX = '_draft.something-else'` and accidentally
184
+ * suppress its own draft-id diagnostic.
185
+ */
186
+ const FRAMEWORK_DRAFT_PREFIX_LITERAL = DRAFT_ID_PREFIX;
187
+ /**
188
+ * Absolute paths of the two framework files allowed to declare the
189
+ * draft-prefix constants. Anchored against the rule module's own URL so the
190
+ * exemption is scoped to this package's real on-disk location — a consumer
191
+ * repository that happens to declare `const DRAFT_ID_PREFIX = '_draft.leak'`
192
+ * anywhere else cannot hide a genuine leak by matching the identifier name.
193
+ *
194
+ * The two framework files are:
195
+ * - `packages/core/src/draft.ts` (defines `DRAFT_ID_PREFIX`)
196
+ * - `packages/warden/src/draft.ts` (defines `DRAFT_FILE_PREFIX`)
197
+ */
198
+ const FRAMEWORK_DRAFT_CONSTANT_FILES = new Set([
199
+ resolve(fileURLToPath(new URL('../../../core/src/draft.ts', import.meta.url))),
200
+ resolve(fileURLToPath(new URL('../draft.ts', import.meta.url))),
201
+ ]);
202
+ /**
203
+ * Collect the source offsets of string literals that initialize a framework
204
+ * draft-prefix constant declaration (e.g. `export const DRAFT_ID_PREFIX =
205
+ * '_draft.'`). Used by draft-awareness rules to skip their own marker
206
+ * constants.
207
+ *
208
+ * Exemption is gated on all three of:
209
+ * 1. The file's absolute path matches one of the two framework files that
210
+ * actually define these constants.
211
+ * 2. The declaration name is `DRAFT_ID_PREFIX` or `DRAFT_FILE_PREFIX`.
212
+ * 3. The string literal value is exactly `'_draft.'`.
213
+ *
214
+ * A consumer file that reuses one of these identifier names cannot hide a
215
+ * `_draft.*` leak — the path gate rejects it outright.
216
+ */
217
+ export const collectFrameworkDraftPrefixConstantOffsets = (ast, filePath) => {
218
+ const offsets = new Set();
219
+ if (!FRAMEWORK_DRAFT_CONSTANT_FILES.has(resolve(filePath))) {
220
+ return offsets;
221
+ }
222
+ walk(ast, (node) => {
223
+ if (node.type !== 'VariableDeclarator') {
224
+ return;
225
+ }
226
+ const { id, init } = node;
227
+ const name = identifierName(id);
228
+ if (!name ||
229
+ !FRAMEWORK_DRAFT_PREFIX_CONSTANT_NAMES.has(name) ||
230
+ !init ||
231
+ !isStringLiteral(init)) {
232
+ return;
233
+ }
234
+ if (getStringValue(init) !== FRAMEWORK_DRAFT_PREFIX_LITERAL) {
235
+ return;
236
+ }
237
+ offsets.add(init.start);
238
+ });
239
+ return offsets;
240
+ };
241
+ const WARDEN_IGNORE_NEXT_LINE_PRAGMA = '// warden-ignore-next-line';
242
+ /**
243
+ * Split source code into lines for pragma lookups. Callers should split once
244
+ * per `check` invocation and thread the result through to
245
+ * {@link hasIgnoreCommentOnLine} so we avoid re-splitting the full source on
246
+ * every match in files with many draft-like string literals.
247
+ */
248
+ export const splitSourceLines = (sourceCode) => sourceCode.split('\n');
249
+ /**
250
+ * Check whether the line immediately preceding `line` contains a
251
+ * `// warden-ignore-next-line` pragma (leading/trailing whitespace tolerated).
252
+ * Pragma scope is strictly one line — an intervening blank line breaks it.
253
+ *
254
+ * Takes a pre-split `lines` array so callers can split the source once per
255
+ * invocation instead of re-splitting for every literal they check.
256
+ *
257
+ * @example
258
+ * ```ts
259
+ * // warden-ignore-next-line
260
+ * const x = '_draft.intentional'; // suppressed
261
+ * ```
262
+ */
263
+ export const hasIgnoreCommentOnLine = (lines, line) => {
264
+ if (line <= 1) {
265
+ return false;
266
+ }
267
+ const previous = lines[line - 2];
268
+ if (previous === undefined) {
269
+ return false;
270
+ }
271
+ return previous.trim() === WARDEN_IGNORE_NEXT_LINE_PRAGMA;
272
+ };
273
+ export const findStringLiterals = (ast, predicate) => {
274
+ const matches = [];
275
+ walk(ast, (node) => {
276
+ if (!isStringLiteral(node)) {
277
+ return;
278
+ }
279
+ const value = getStringValue(node);
280
+ if (value === null) {
281
+ return;
282
+ }
283
+ if (predicate && !predicate(value, node)) {
284
+ return;
285
+ }
286
+ matches.push({
287
+ end: node.end,
288
+ node,
289
+ start: node.start,
290
+ value,
291
+ });
292
+ });
293
+ return matches;
294
+ };
118
295
  /** Extract the first string argument from a CallExpression. */
119
296
  export const extractFirstStringArg = (node) => {
120
297
  if (node.type !== 'CallExpression') {
@@ -124,10 +301,10 @@ export const extractFirstStringArg = (node) => {
124
301
  const [firstArg] = args ?? [];
125
302
  return extractStringLiteral(firstArg);
126
303
  };
127
- const isProvisionCall = (node) => !!node &&
304
+ const isResourceCall = (node) => !!node &&
128
305
  node.type === 'CallExpression' &&
129
306
  identifierName(node.callee) ===
130
- 'provision';
307
+ 'resource';
131
308
  const extractBindingName = (node) => {
132
309
  if (!node) {
133
310
  return null;
@@ -140,30 +317,30 @@ const extractBindingName = (node) => {
140
317
  }
141
318
  return null;
142
319
  };
143
- /** Collect `const foo = provision('id', ...)` bindings from a parsed file. */
144
- export const collectNamedProvisionIds = (ast) => {
320
+ /** Collect `const foo = resource('id', ...)` bindings from a parsed file. */
321
+ export const collectNamedResourceIds = (ast) => {
145
322
  const ids = new Map();
146
323
  walk(ast, (node) => {
147
324
  if (node.type !== 'VariableDeclarator') {
148
325
  return;
149
326
  }
150
327
  const { id, init } = node;
151
- if (!isProvisionCall(init)) {
328
+ if (!isResourceCall(init)) {
152
329
  return;
153
330
  }
154
331
  const name = extractBindingName(id);
155
- const provisionId = init ? extractFirstStringArg(init) : null;
156
- if (name && provisionId) {
157
- ids.set(name, provisionId);
332
+ const resourceId = init ? extractFirstStringArg(init) : null;
333
+ if (name && resourceId) {
334
+ ids.set(name, resourceId);
158
335
  }
159
336
  });
160
337
  return ids;
161
338
  };
162
- /** Collect all inline `provision('id', ...)` definition IDs from a parsed file. */
163
- export const collectProvisionDefinitionIds = (ast) => {
339
+ /** Collect all inline `resource('id', ...)` definition IDs from a parsed file. */
340
+ export const collectResourceDefinitionIds = (ast) => {
164
341
  const ids = new Set();
165
342
  walk(ast, (node) => {
166
- if (!isProvisionCall(node)) {
343
+ if (!isResourceCall(node)) {
167
344
  return;
168
345
  }
169
346
  const id = extractFirstStringArg(node);
@@ -174,12 +351,36 @@ export const collectProvisionDefinitionIds = (ast) => {
174
351
  return ids;
175
352
  };
176
353
  /** Backward-compatible aliases while the migration is in flight. */
177
- export const collectNamedServiceIds = collectNamedProvisionIds;
354
+ export const collectNamedServiceIds = collectNamedResourceIds;
178
355
  /** Backward-compatible aliases while the migration is in flight. */
179
- export const collectServiceDefinitionIds = collectProvisionDefinitionIds;
356
+ export const collectServiceDefinitionIds = collectResourceDefinitionIds;
180
357
  // ---------------------------------------------------------------------------
181
358
  // Config property extraction helpers
182
359
  // ---------------------------------------------------------------------------
360
+ /**
361
+ * Extract the identifying name of a `Property` key, supporting both
362
+ * identifier keys (`{ foo: 1 }`) and string-literal keys
363
+ * (`{ "foo": 1 }`). Computed keys are intentionally not resolved — a
364
+ * computed expression could evaluate to anything and we only want to
365
+ * match keys that are statically equivalent to a plain identifier.
366
+ */
367
+ const staticPropertyKeyName = (key) => {
368
+ if (key.type === 'Identifier') {
369
+ return key.name ?? null;
370
+ }
371
+ return isStringLiteral(key) ? getStringValue(key) : null;
372
+ };
373
+ const propertyKeyName = (prop) => {
374
+ if (prop.type !== 'Property') {
375
+ return null;
376
+ }
377
+ const { computed } = prop;
378
+ if (computed) {
379
+ return null;
380
+ }
381
+ const key = prop.key;
382
+ return key ? staticPropertyKeyName(key) : null;
383
+ };
183
384
  /** Find a Property node by key name inside an ObjectExpression config. */
184
385
  export const findConfigProperty = (config, propertyName) => {
185
386
  if (config.type !== 'ObjectExpression') {
@@ -190,7 +391,7 @@ export const findConfigProperty = (config, propertyName) => {
190
391
  return null;
191
392
  }
192
393
  for (const prop of properties) {
193
- if (prop.type === 'Property' && prop.key?.name === propertyName) {
394
+ if (propertyKeyName(prop) === propertyName) {
194
395
  return prop;
195
396
  }
196
397
  }
@@ -202,121 +403,1665 @@ export const findConfigProperty = (config, propertyName) => {
202
403
  *
203
404
  * Returns the trail ID, kind, and config object node for each definition.
204
405
  */
205
- const TRAIL_CALLEE_NAMES = new Set(['trail', 'signal']);
206
- const getTrailCalleeName = (node) => {
207
- if (node.type !== 'CallExpression') {
208
- return null;
406
+ const TRAIL_CALLEE_NAMES = new Set(['signal', 'trail']);
407
+ /**
408
+ * Source prefix for the Trails framework package whose namespace imports are
409
+ * recognized as carriers of `trail()` / `signal()` / `contour()` primitives.
410
+ *
411
+ * A namespaced callee like `core.trail(...)` is only treated as a framework
412
+ * call when the receiver identifier resolves to an `import * as core from
413
+ * '@ontrails/...'` in the same file. An unrelated `analytics.trail(...)`
414
+ * whose `analytics` comes from a different module (or no import at all)
415
+ * is ignored.
416
+ */
417
+ const FRAMEWORK_NAMESPACE_SOURCE_PREFIX = '@ontrails/';
418
+ const isFrameworkNamespaceSource = (value) => typeof value === 'string' &&
419
+ value.startsWith(FRAMEWORK_NAMESPACE_SOURCE_PREFIX);
420
+ /**
421
+ * Collect local binding names introduced by `import * as <name> from
422
+ * '@ontrails/...'` declarations. Used to gate namespaced framework-primitive
423
+ * calls so an unrelated `analytics.trail(...)` doesn't match.
424
+ */
425
+ const getImportSourceValue = (node) => {
426
+ const sourceNode = node.source;
427
+ return sourceNode
428
+ ? sourceNode.value
429
+ : undefined;
430
+ };
431
+ const addNamespaceImportBindings = (node, names) => {
432
+ const specifiers = node['specifiers'] ?? [];
433
+ for (const spec of specifiers) {
434
+ if (spec.type !== 'ImportNamespaceSpecifier') {
435
+ continue;
436
+ }
437
+ const { local } = spec;
438
+ const localName = identifierName(local);
439
+ if (localName) {
440
+ names.add(localName);
441
+ }
209
442
  }
210
- const callee = node['callee'];
211
- if (!callee || callee.type !== 'Identifier') {
212
- return null;
443
+ };
444
+ const TOP_LEVEL_NAMED_DECL_TYPES = new Set([
445
+ 'ClassDeclaration',
446
+ 'FunctionDeclaration',
447
+ 'TSEnumDeclaration',
448
+ 'TSModuleDeclaration',
449
+ ]);
450
+ const removeVarDeclarationShadowedNames = (stmt, names) => {
451
+ const declarations = stmt.declarations ??
452
+ [];
453
+ for (const d of declarations) {
454
+ const { id } = d;
455
+ const n = identifierName(id);
456
+ if (n) {
457
+ names.delete(n);
458
+ }
213
459
  }
214
- const { name } = callee;
215
- return name && TRAIL_CALLEE_NAMES.has(name) ? name : null;
216
460
  };
217
- /** Extract args from a trail() call, handling both two-arg and single-object forms. */
218
- const extractTrailArgs = (node) => {
219
- const args = node['arguments'];
220
- if (!args || args.length === 0) {
221
- return null;
461
+ const removeNamedDeclShadowedName = (stmt, names) => {
462
+ const { id } = stmt;
463
+ const n = identifierName(id);
464
+ if (n) {
465
+ names.delete(n);
222
466
  }
223
- const [firstArg, secondArg] = args;
224
- if (!firstArg) {
225
- return null;
467
+ };
468
+ const removeTopLevelShadowedNames = (stmt, names) => {
469
+ if (stmt.type === 'ExportNamedDeclaration' ||
470
+ stmt.type === 'ExportDefaultDeclaration') {
471
+ const { declaration } = stmt;
472
+ if (declaration) {
473
+ removeTopLevelShadowedNames(declaration, names);
474
+ }
475
+ return;
226
476
  }
227
- // Two-arg form: trail('id', { ... })
228
- if (secondArg && firstArg.type !== 'ObjectExpression') {
229
- return { configArg: secondArg, idArg: firstArg };
477
+ if (stmt.type === 'VariableDeclaration') {
478
+ removeVarDeclarationShadowedNames(stmt, names);
479
+ return;
480
+ }
481
+ if (TOP_LEVEL_NAMED_DECL_TYPES.has(stmt.type)) {
482
+ removeNamedDeclShadowedName(stmt, names);
230
483
  }
231
- // Single-object form: trail({ id: 'x', ... })
232
- return firstArg.type === 'ObjectExpression'
233
- ? { configArg: firstArg, idArg: null }
234
- : null;
235
484
  };
236
- /** Extract the string value from an `id` property inside a config ObjectExpression. */
237
- const extractIdFromConfig = (config) => {
238
- const idProp = findConfigProperty(config, 'id');
239
- if (!idProp || !idProp.value) {
240
- return null;
485
+ const collectFrameworkNamespaceBindings = (ast) => {
486
+ const names = new Set();
487
+ walk(ast, (node) => {
488
+ if (node.type !== 'ImportDeclaration') {
489
+ return;
490
+ }
491
+ if (!isFrameworkNamespaceSource(getImportSourceValue(node))) {
492
+ return;
493
+ }
494
+ addNamespaceImportBindings(node, names);
495
+ });
496
+ if (names.size === 0) {
497
+ return names;
241
498
  }
242
- const val = idProp.value.value;
243
- return typeof val === 'string' ? val : null;
499
+ // A same-named top-level declaration (class / enum / namespace / var /
500
+ // function / lexical binding) shadows the namespace import at module scope.
501
+ // The scope walker treats Program as the outermost frame and skips it when
502
+ // testing for inner shadows, so we have to strip these collisions here.
503
+ if (ast.type === 'Program') {
504
+ const body = ast.body ?? [];
505
+ for (const stmt of body) {
506
+ removeTopLevelShadowedNames(stmt, names);
507
+ }
508
+ }
509
+ return names;
244
510
  };
245
- const extractTrailId = (trailArgs) => {
246
- if (trailArgs.idArg) {
247
- return trailArgs.idArg.value ?? null;
511
+ const expandAssignmentPattern = (node) => {
512
+ const { left } = node;
513
+ return left ? [left] : [];
514
+ };
515
+ const expandRestElement = (node) => {
516
+ const { argument } = node;
517
+ return argument ? [argument] : [];
518
+ };
519
+ const expandArrayPattern = (node) => {
520
+ const elements = node.elements ??
521
+ [];
522
+ return elements.filter((e) => e !== null);
523
+ };
524
+ const expandObjectPatternProperty = (prop) => {
525
+ if (prop.type === 'RestElement') {
526
+ return prop;
248
527
  }
249
- return extractIdFromConfig(trailArgs.configArg);
528
+ const { value } = prop;
529
+ return value ?? null;
250
530
  };
251
- const extractTrailDefinition = (node) => {
252
- const calleeName = getTrailCalleeName(node);
253
- if (!calleeName) {
254
- return null;
531
+ const expandObjectPattern = (node) => {
532
+ const properties = node.properties ?? [];
533
+ return properties
534
+ .map(expandObjectPatternProperty)
535
+ .filter((n) => n !== null);
536
+ };
537
+ const PATTERN_EXPANDERS = {
538
+ ArrayPattern: expandArrayPattern,
539
+ AssignmentPattern: expandAssignmentPattern,
540
+ ObjectPattern: expandObjectPattern,
541
+ RestElement: expandRestElement,
542
+ };
543
+ const processPatternNode = (node, into, stack) => {
544
+ if (node.type === 'Identifier') {
545
+ const { name } = node;
546
+ if (name) {
547
+ into.add(name);
548
+ }
549
+ return;
255
550
  }
256
- const trailArgs = extractTrailArgs(node);
257
- if (!trailArgs) {
258
- return null;
551
+ const expand = PATTERN_EXPANDERS[node.type];
552
+ if (expand) {
553
+ stack.push(...expand(node));
259
554
  }
260
- const trailId = extractTrailId(trailArgs);
261
- if (!trailId) {
262
- return null;
555
+ };
556
+ const addPatternBindingNames = (pattern, into) => {
557
+ if (!pattern) {
558
+ return;
559
+ }
560
+ const stack = [pattern];
561
+ while (stack.length > 0) {
562
+ const node = stack.pop();
563
+ if (node) {
564
+ processPatternNode(node, into, stack);
565
+ }
263
566
  }
264
- return {
265
- config: trailArgs.configArg,
266
- id: trailId,
267
- kind: calleeName,
268
- start: node.start,
269
- };
270
567
  };
271
- export const findTrailDefinitions = (ast) => {
272
- const definitions = [];
273
- walk(ast, (node) => {
274
- const def = extractTrailDefinition(node);
275
- if (def) {
276
- definitions.push(def);
568
+ const addVarDeclarationBindingNames = (decl, into) => {
569
+ const declarations = decl.declarations ??
570
+ [];
571
+ for (const d of declarations) {
572
+ addPatternBindingNames(d.id, into);
573
+ }
574
+ };
575
+ const addFunctionOrClassBindingName = (node, into) => {
576
+ const { id } = node;
577
+ const name = identifierName(id);
578
+ if (name) {
579
+ into.add(name);
580
+ }
581
+ };
582
+ const addBlockStatementBindings = (stmt, into) => {
583
+ if (stmt.type === 'VariableDeclaration') {
584
+ addVarDeclarationBindingNames(stmt, into);
585
+ return;
586
+ }
587
+ if (stmt.type === 'FunctionDeclaration' ||
588
+ stmt.type === 'ClassDeclaration' ||
589
+ stmt.type === 'TSEnumDeclaration' ||
590
+ stmt.type === 'TSModuleDeclaration') {
591
+ addFunctionOrClassBindingName(stmt, into);
592
+ }
593
+ };
594
+ const collectTopLevelStatementBindings = (stmt, into) => {
595
+ if (stmt.type === 'ExportNamedDeclaration' ||
596
+ stmt.type === 'ExportDefaultDeclaration') {
597
+ const { declaration } = stmt;
598
+ if (declaration) {
599
+ collectTopLevelStatementBindings(declaration, into);
600
+ }
601
+ return;
602
+ }
603
+ addBlockStatementBindings(stmt, into);
604
+ };
605
+ const FUNCTION_BOUNDARY_TYPES = new Set([
606
+ 'ArrowFunctionExpression',
607
+ 'FunctionDeclaration',
608
+ 'FunctionExpression',
609
+ 'StaticBlock',
610
+ ]);
611
+ const forEachAstChild = (node, visit) => {
612
+ for (const val of Object.values(node)) {
613
+ if (Array.isArray(val)) {
614
+ for (const item of val) {
615
+ if (item && typeof item === 'object' && item.type) {
616
+ visit(item);
617
+ }
618
+ }
619
+ }
620
+ else if (val && typeof val === 'object' && val.type) {
621
+ visit(val);
622
+ }
623
+ }
624
+ };
625
+ const recordHoistedBinding = (node, into, inNestedBlock) => {
626
+ if (node.type === 'VariableDeclaration') {
627
+ const { kind } = node;
628
+ if (kind === 'var') {
629
+ addVarDeclarationBindingNames(node, into);
277
630
  }
631
+ return;
632
+ }
633
+ // In strict/module code, function/class/enum/module declarations inside a
634
+ // nested block (`if { function foo() {} }`, `switch` case, etc.) are
635
+ // block-scoped. Only hoist them to the enclosing function frame when they
636
+ // sit directly in the function body, not inside a further block.
637
+ if (inNestedBlock) {
638
+ return;
639
+ }
640
+ if (node.type === 'FunctionDeclaration' ||
641
+ node.type === 'ClassDeclaration' ||
642
+ node.type === 'TSEnumDeclaration' ||
643
+ node.type === 'TSModuleDeclaration') {
644
+ addFunctionOrClassBindingName(node, into);
645
+ }
646
+ };
647
+ const NESTED_BLOCK_BOUNDARY_TYPES = new Set([
648
+ 'BlockStatement',
649
+ 'ForStatement',
650
+ 'ForInStatement',
651
+ 'ForOfStatement',
652
+ 'SwitchStatement',
653
+ 'CatchClause',
654
+ ]);
655
+ const visitForHoisted = (node, isRoot, into, inNestedBlock) => {
656
+ if (!isRoot && FUNCTION_BOUNDARY_TYPES.has(node.type)) {
657
+ return;
658
+ }
659
+ recordHoistedBinding(node, into, inNestedBlock);
660
+ const childInNestedBlock = inNestedBlock || (!isRoot && NESTED_BLOCK_BOUNDARY_TYPES.has(node.type));
661
+ forEachAstChild(node, (child) => {
662
+ visitForHoisted(child, false, into, childInNestedBlock);
278
663
  });
279
- return definitions;
280
664
  };
281
- // ---------------------------------------------------------------------------
282
- // Blaze body extraction
283
- // ---------------------------------------------------------------------------
284
665
  /**
285
- * Extract top-level `blaze:` property values from an ObjectExpression's direct properties.
286
- *
287
- * Does not recurse into nested objects, so `meta: { blaze: ... }` is ignored.
666
+ * Collect `var` declarations and `function` declarations hoisted to the
667
+ * nearest function scope from anywhere inside `root`, without crossing a
668
+ * nested function or static-block boundary.
288
669
  */
289
- const extractBlazeFromConfig = (config) => {
290
- const bodies = [];
291
- const properties = config['properties'];
292
- if (!properties) {
293
- return bodies;
670
+ const collectHoistedVarAndFunctionBindings = (root, into) => {
671
+ visitForHoisted(root, true, into, false);
672
+ };
673
+ const collectProgramFrame = (node, into) => {
674
+ const body = node.body ?? [];
675
+ for (const stmt of body) {
676
+ collectTopLevelStatementBindings(stmt, into);
294
677
  }
295
- for (const prop of properties) {
296
- if (prop.type === 'Property' && prop.key?.name === 'blaze' && prop.value) {
297
- bodies.push(prop.value);
678
+ };
679
+ const collectFunctionFrame = (node, into) => {
680
+ const params = node.params ?? [];
681
+ for (const param of params) {
682
+ addPatternBindingNames(param, into);
683
+ }
684
+ // Hoisted vars and function declarations inside the body live in the
685
+ // function's var-environment. A `var ns = ...;` inside an `if` still
686
+ // shadows a module-level `ns` for the whole function.
687
+ const { body } = node;
688
+ if (body) {
689
+ collectHoistedVarAndFunctionBindings(body, into);
690
+ }
691
+ };
692
+ const collectBlockFrame = (node, into) => {
693
+ const body = node.body ?? [];
694
+ for (const stmt of body) {
695
+ addBlockStatementBindings(stmt, into);
696
+ }
697
+ };
698
+ const collectForStatementFrame = (node, into) => {
699
+ const { init } = node;
700
+ if (init && init.type === 'VariableDeclaration') {
701
+ addVarDeclarationBindingNames(init, into);
702
+ }
703
+ };
704
+ const collectForInOfFrame = (node, into) => {
705
+ const { left } = node;
706
+ if (left && left.type === 'VariableDeclaration') {
707
+ addVarDeclarationBindingNames(left, into);
708
+ }
709
+ };
710
+ const collectSwitchStatementFrame = (node, into) => {
711
+ // `switch` shares one scope across every case. A binding in one case
712
+ // shadows the namespace across sibling cases (fall-through or otherwise).
713
+ const cases = node.cases ?? [];
714
+ for (const c of cases) {
715
+ const consequent = c.consequent ?? [];
716
+ for (const stmt of consequent) {
717
+ addBlockStatementBindings(stmt, into);
298
718
  }
299
719
  }
300
- return bodies;
720
+ };
721
+ const collectCatchClauseFrame = (node, into) => {
722
+ const { param } = node;
723
+ addPatternBindingNames(param, into);
724
+ };
725
+ const collectClassExpressionFrame = (node, into) => {
726
+ // A named `class expr` (`const C = class foo { ... }`) binds its own name
727
+ // inside its body only. ClassDeclaration names are hoisted into the
728
+ // enclosing block/program frame instead, so only class *expression* names
729
+ // need their own frame here.
730
+ addFunctionOrClassBindingName(node, into);
731
+ };
732
+ const SCOPE_FRAME_COLLECTORS = {
733
+ ArrowFunctionExpression: collectFunctionFrame,
734
+ BlockStatement: collectBlockFrame,
735
+ CatchClause: collectCatchClauseFrame,
736
+ ClassExpression: collectClassExpressionFrame,
737
+ ForInStatement: collectForInOfFrame,
738
+ ForOfStatement: collectForInOfFrame,
739
+ ForStatement: collectForStatementFrame,
740
+ // oxc-parser emits `FunctionBody` for `function` expression bodies; without
741
+ // this entry, a `const ns = ...` at the top of a function-expression body
742
+ // would not push a scope frame, and a module-level namespace import with
743
+ // the same name would be incorrectly recognized inside.
744
+ FunctionBody: collectBlockFrame,
745
+ FunctionDeclaration: collectFunctionFrame,
746
+ FunctionExpression: collectFunctionFrame,
747
+ Program: collectProgramFrame,
748
+ StaticBlock: collectBlockFrame,
749
+ SwitchStatement: collectSwitchStatementFrame,
301
750
  };
302
751
  /**
303
- * Find `blaze:` property values.
304
- *
305
- * When given an ObjectExpression (trail config), returns only its direct `blaze:`
306
- * properties. When given a full AST, finds trail definitions first and extracts
307
- * `blaze:` from each config — in both cases ignoring nested `blaze:` properties
308
- * (e.g. `meta: { blaze: ... }`).
752
+ * Collect the identifier bindings introduced *directly* by a scope frame
753
+ * node. Scope frames correspond to JS lexical scopes (function bodies, blocks,
754
+ * catch clauses, for-statements, switch statements, module/script roots).
309
755
  */
310
- export const findBlazeBodies = (node) => {
311
- if (node.type === 'ObjectExpression') {
312
- return extractBlazeFromConfig(node);
756
+ export const collectScopeFrameBindings = (node) => {
757
+ const names = new Set();
758
+ const collector = SCOPE_FRAME_COLLECTORS[node.type];
759
+ if (collector) {
760
+ collector(node, names);
313
761
  }
314
- // Full AST — find trail definitions and extract blaze from their configs
315
- const bodies = [];
316
- for (const def of findTrailDefinitions(node)) {
317
- bodies.push(...extractBlazeFromConfig(def.config));
762
+ return names;
763
+ };
764
+ const asAstNode = (node) => {
765
+ if (!node || typeof node !== 'object') {
766
+ return null;
318
767
  }
319
- return bodies;
768
+ const astNode = node;
769
+ return astNode.type ? astNode : null;
770
+ };
771
+ /**
772
+ * Walk an AST subtree while threading lexical scope bindings through each
773
+ * visit. Callers can seed outer scopes and optionally stop at nested function
774
+ * boundaries when only the current implementation body should be analyzed.
775
+ */
776
+ export const walkWithScopes = (node, visit, options = {}) => {
777
+ const root = asAstNode(node);
778
+ if (!root) {
779
+ return;
780
+ }
781
+ const stack = [...(options.initialScopes ?? [])];
782
+ const walkNode = (current, isRoot) => {
783
+ if (!isRoot &&
784
+ options.stopAtNestedFunctions &&
785
+ FUNCTION_BOUNDARY_TYPES.has(current.type)) {
786
+ return;
787
+ }
788
+ const isScope = current.type in SCOPE_FRAME_COLLECTORS;
789
+ if (isScope) {
790
+ stack.unshift(collectScopeFrameBindings(current));
791
+ }
792
+ try {
793
+ visit(current, stack);
794
+ forEachAstChild(current, (child) => {
795
+ walkNode(child, false);
796
+ });
797
+ }
798
+ finally {
799
+ if (isScope) {
800
+ stack.shift();
801
+ }
802
+ }
803
+ };
804
+ walkNode(root, true);
805
+ };
806
+ const isShadowed = (receiverName, scopeStack) => {
807
+ // The module-level Program frame is the last entry and contains the
808
+ // namespace imports themselves. A "shadow" must come from a frame *inside*
809
+ // that one — i.e. any frame except the outermost.
810
+ for (let i = 0; i < scopeStack.length - 1; i += 1) {
811
+ const frame = scopeStack[i];
812
+ if (frame?.has(receiverName)) {
813
+ return true;
814
+ }
815
+ }
816
+ return false;
817
+ };
818
+ /**
819
+ * Return `true` when `node` is a non-computed member access (`a.b` /
820
+ * `a?.b`) and `false` for anything else, including computed access
821
+ * (`a[b]`) or non-member nodes. Exported as the canonical predicate so
822
+ * rule modules do not re-implement the check.
823
+ *
824
+ * @remarks
825
+ * Declared near the top of the file so the scope walker can use it
826
+ * without hitting `no-use-before-define`. A few sibling helpers in this
827
+ * module still inline the same shape under different local names for
828
+ * historical reasons; prefer this export for new call sites.
829
+ */
830
+ export const isMemberAccessNonComputed = (node) => {
831
+ if (node.type !== 'MemberExpression' &&
832
+ node.type !== 'StaticMemberExpression') {
833
+ return false;
834
+ }
835
+ return node.computed !== true;
836
+ };
837
+ const resolveNamespacedMemberNames = (callee) => {
838
+ if (!isMemberAccessNonComputed(callee)) {
839
+ return null;
840
+ }
841
+ const { object } = callee;
842
+ const receiver = identifierName(object);
843
+ if (!receiver) {
844
+ return null;
845
+ }
846
+ const prop = callee.property;
847
+ const property = prop?.type === 'Identifier'
848
+ ? (prop.name ?? null)
849
+ : null;
850
+ return property ? { property, receiver } : null;
851
+ };
852
+ const getFrameworkCallReceiver = (node, frameworkNamespaces) => {
853
+ if (node.type !== 'CallExpression') {
854
+ return null;
855
+ }
856
+ const callee = node['callee'];
857
+ if (!callee) {
858
+ return null;
859
+ }
860
+ const names = resolveNamespacedMemberNames(callee);
861
+ if (!names || !frameworkNamespaces.has(names.receiver)) {
862
+ return null;
863
+ }
864
+ return names.receiver;
865
+ };
866
+ /**
867
+ * Walk the AST with a scope stack and collect `CallExpression` start offsets
868
+ * whose callee is `<receiver>.<property>` where `<receiver>` is proven to
869
+ * resolve to a framework namespace import (i.e. not shadowed by any
870
+ * enclosing scope). Used to gate namespaced `core.trail(...)` /
871
+ * `core.signal(...)` / `core.contour(...)` resolution against local shadows.
872
+ */
873
+ const collectFrameworkNamespacedCallStarts = (ast, frameworkNamespaces) => {
874
+ const starts = new Set();
875
+ if (frameworkNamespaces.size === 0) {
876
+ return starts;
877
+ }
878
+ walkWithScopes(ast, (node, scopes) => {
879
+ const receiver = getFrameworkCallReceiver(node, frameworkNamespaces);
880
+ if (!receiver || isShadowed(receiver, scopes)) {
881
+ return;
882
+ }
883
+ starts.add(node.start);
884
+ });
885
+ return starts;
886
+ };
887
+ const matchTrailPrimitiveName = (name) => (name && TRAIL_CALLEE_NAMES.has(name) ? name : null);
888
+ const getBareTrailCalleeName = (callee) => {
889
+ if (callee.type !== 'Identifier') {
890
+ return null;
891
+ }
892
+ return matchTrailPrimitiveName(callee.name);
893
+ };
894
+ /**
895
+ * Extract the `{ receiverName, propertyName }` of a non-computed member-call
896
+ * callee, or null for anything else. Computed access (`ns[trail]()`) is
897
+ * intentionally rejected: the bracketed expression may resolve to any runtime
898
+ * value, so we cannot prove the call targets a specific member.
899
+ */
900
+ const isNonComputedMemberAccess = (callee) => {
901
+ if (callee.type !== 'MemberExpression' &&
902
+ callee.type !== 'StaticMemberExpression') {
903
+ return false;
904
+ }
905
+ return callee.computed !== true;
906
+ };
907
+ const getNamespacedMemberNames = (callee) => {
908
+ if (!isNonComputedMemberAccess(callee)) {
909
+ return null;
910
+ }
911
+ const { object } = callee;
912
+ const receiver = identifierName(object);
913
+ if (!receiver) {
914
+ return null;
915
+ }
916
+ const prop = callee.property;
917
+ const property = prop?.type === 'Identifier'
918
+ ? (prop.name ?? null)
919
+ : null;
920
+ return property ? { property, receiver } : null;
921
+ };
922
+ const asNamespaceContext = (input) => {
923
+ if (!input) {
924
+ return undefined;
925
+ }
926
+ return input instanceof Set
927
+ ? { namespaces: input }
928
+ : input;
929
+ };
930
+ const isNamespacedCallAllowed = (callStart, receiver, ctx) => {
931
+ if (!ctx.namespaces.has(receiver)) {
932
+ return false;
933
+ }
934
+ // When `safeCallStarts` is present, it is the authoritative gate — it was
935
+ // built by a scope-aware pre-pass and already excludes shadowed receivers.
936
+ // Without it, fall back to the bare name check (used by unit-test hooks).
937
+ return ctx.safeCallStarts ? ctx.safeCallStarts.has(callStart) : true;
938
+ };
939
+ /**
940
+ * Resolve a namespaced `ns.trail(...)` / `ns.signal(...)` callee to its
941
+ * primitive name. When a {@link FrameworkNamespaceContext} is provided, the
942
+ * receiver must be a framework namespace binding AND — when a
943
+ * `safeCallStarts` set is present — the call site must appear in that set,
944
+ * meaning the receiver is not shadowed by any enclosing scope.
945
+ *
946
+ * When `context` is `undefined`, this falls back to permissive matching
947
+ * (any `ns.trail(...)` shape resolves). Inline resolution paths that do
948
+ * not have the surrounding AST available (e.g. `crosses: [core.trail(...)]`
949
+ * or `on: [core.signal(...)]`) rely on this fallback. Scope-aware call
950
+ * sites always pass a context, so this only affects inline contexts where
951
+ * a best-effort name match is the intended behavior.
952
+ */
953
+ const getNamespacedTrailCalleeName = (callExpr, callee, context) => {
954
+ const names = getNamespacedMemberNames(callee);
955
+ if (!names) {
956
+ return null;
957
+ }
958
+ const ctx = asNamespaceContext(context);
959
+ if (ctx && !isNamespacedCallAllowed(callExpr.start, names.receiver, ctx)) {
960
+ return null;
961
+ }
962
+ return matchTrailPrimitiveName(names.property);
963
+ };
964
+ /**
965
+ * Resolve the callee name of a trail/signal call expression.
966
+ *
967
+ * Matches both bare `trail(...)` / `signal(...)` identifiers and namespaced
968
+ * member-expression callees like `core.trail(...)` or `ns.signal(...)`, where
969
+ * the namespace must come from an `@ontrails/*` import and, when the scope
970
+ * pre-pass is wired in, be unshadowed at the call site.
971
+ */
972
+ const getTrailCalleeName = (node, context) => {
973
+ if (node.type !== 'CallExpression') {
974
+ return null;
975
+ }
976
+ const callee = node['callee'];
977
+ if (!callee) {
978
+ return null;
979
+ }
980
+ return (getBareTrailCalleeName(callee) ??
981
+ getNamespacedTrailCalleeName(node, callee, context));
982
+ };
983
+ /**
984
+ * Test hook: exposes {@link getTrailCalleeName} for unit tests.
985
+ *
986
+ * Kept unexported from the module's public surface (no re-export from
987
+ * `index.ts`) so internal refactors stay free.
988
+ */
989
+ export const __getTrailCalleeNameForTest = getTrailCalleeName;
990
+ /**
991
+ * Test hook: exposes {@link collectFrameworkNamespaceBindings} for unit tests.
992
+ *
993
+ * Not re-exported from `index.ts`; the double-underscore prefix marks it as an
994
+ * internal-only handle so consumer code cannot rely on it.
995
+ */
996
+ export const __collectFrameworkNamespaceBindingsForTest = collectFrameworkNamespaceBindings;
997
+ /** Extract args from a trail() call, handling both two-arg and single-object forms. */
998
+ const extractTrailArgs = (node) => {
999
+ const args = node['arguments'];
1000
+ if (!args || args.length === 0) {
1001
+ return null;
1002
+ }
1003
+ const [firstArg, secondArg] = args;
1004
+ if (!firstArg) {
1005
+ return null;
1006
+ }
1007
+ // Two-arg form: trail('id', { ... })
1008
+ if (secondArg && firstArg.type !== 'ObjectExpression') {
1009
+ return { configArg: secondArg, idArg: firstArg };
1010
+ }
1011
+ // Single-object form: trail({ id: 'x', ... })
1012
+ return firstArg.type === 'ObjectExpression'
1013
+ ? { configArg: firstArg, idArg: null }
1014
+ : null;
1015
+ };
1016
+ /** Extract the string value from an `id` property inside a config ObjectExpression. */
1017
+ const extractIdFromConfig = (config) => {
1018
+ const idProp = findConfigProperty(config, 'id');
1019
+ if (!idProp || !idProp.value) {
1020
+ return null;
1021
+ }
1022
+ return extractStringOrTemplateLiteral(idProp.value);
1023
+ };
1024
+ const extractTrailId = (trailArgs) => {
1025
+ if (trailArgs.idArg) {
1026
+ return extractStringOrTemplateLiteral(trailArgs.idArg);
1027
+ }
1028
+ return extractIdFromConfig(trailArgs.configArg);
1029
+ };
1030
+ const extractTrailDefinition = (node, context) => {
1031
+ const calleeName = getTrailCalleeName(node, context);
1032
+ if (!calleeName) {
1033
+ return null;
1034
+ }
1035
+ const trailArgs = extractTrailArgs(node);
1036
+ if (!trailArgs) {
1037
+ return null;
1038
+ }
1039
+ const trailId = extractTrailId(trailArgs);
1040
+ if (!trailId) {
1041
+ return null;
1042
+ }
1043
+ return {
1044
+ config: trailArgs.configArg,
1045
+ id: trailId,
1046
+ kind: calleeName,
1047
+ start: node.start,
1048
+ };
1049
+ };
1050
+ const buildFrameworkNamespaceContext = (ast) => {
1051
+ const namespaces = collectFrameworkNamespaceBindings(ast);
1052
+ return {
1053
+ namespaces,
1054
+ safeCallStarts: collectFrameworkNamespacedCallStarts(ast, namespaces),
1055
+ };
1056
+ };
1057
+ export const findTrailDefinitions = (ast) => {
1058
+ const definitions = [];
1059
+ const context = buildFrameworkNamespaceContext(ast);
1060
+ walk(ast, (node) => {
1061
+ const def = extractTrailDefinition(node, context);
1062
+ if (def) {
1063
+ definitions.push(def);
1064
+ }
1065
+ });
1066
+ return definitions;
1067
+ };
1068
+ const CONTOUR_PRIMITIVE_NAME = 'contour';
1069
+ const matchContourPrimitiveName = (name) => (name === CONTOUR_PRIMITIVE_NAME ? name : null);
1070
+ const getBareContourCalleeName = (callee) => {
1071
+ if (callee.type !== 'Identifier') {
1072
+ return null;
1073
+ }
1074
+ return matchContourPrimitiveName(callee.name);
1075
+ };
1076
+ /**
1077
+ * Resolve a namespaced `ns.contour(...)` callee to its primitive name. Mirrors
1078
+ * {@link getNamespacedTrailCalleeName}: the receiver identifier must resolve
1079
+ * to an `@ontrails/*` namespace import, and — when a scope-aware
1080
+ * `safeCallStarts` set is provided — the call site must not be shadowed by a
1081
+ * local binding of the same name.
1082
+ */
1083
+ const getNamespacedContourCalleeName = (callExpr, callee, context) => {
1084
+ const names = getNamespacedMemberNames(callee);
1085
+ if (!names) {
1086
+ return null;
1087
+ }
1088
+ // Unlike the trail/signal variant, contour has no inline-resolution callers
1089
+ // that legitimately invoke this without a FrameworkNamespaceContext, so the
1090
+ // strict namespace gate stays on. If a future caller needs the permissive
1091
+ // fallback, mirror the trail shape and add a regression test first.
1092
+ const ctx = asNamespaceContext(context);
1093
+ if (!ctx || !isNamespacedCallAllowed(callExpr.start, names.receiver, ctx)) {
1094
+ return null;
1095
+ }
1096
+ return matchContourPrimitiveName(names.property);
1097
+ };
1098
+ /**
1099
+ * Resolve the callee name of a contour call expression. Matches both bare
1100
+ * `contour(...)` identifiers and namespaced `core.contour(...)` callees where
1101
+ * the namespace comes from an `@ontrails/*` import and is unshadowed.
1102
+ */
1103
+ const getContourCalleeName = (node, context) => {
1104
+ if (node.type !== 'CallExpression') {
1105
+ return null;
1106
+ }
1107
+ const callee = node['callee'];
1108
+ if (!callee) {
1109
+ return null;
1110
+ }
1111
+ return (getBareContourCalleeName(callee) ??
1112
+ getNamespacedContourCalleeName(node, callee, context));
1113
+ };
1114
+ const extractContourDefinition = (node, context) => {
1115
+ if (!getContourCalleeName(node, context)) {
1116
+ return null;
1117
+ }
1118
+ const args = node['arguments'];
1119
+ const [nameArg, shapeArg, optionsArg] = args ?? [];
1120
+ const name = extractStringLiteral(nameArg);
1121
+ if (!name || shapeArg?.type !== 'ObjectExpression') {
1122
+ return null;
1123
+ }
1124
+ return {
1125
+ call: node,
1126
+ name,
1127
+ options: optionsArg?.type === 'ObjectExpression' ? optionsArg : null,
1128
+ shape: shapeArg,
1129
+ start: node.start,
1130
+ };
1131
+ };
1132
+ const getCallStartFromCandidate = (node) => {
1133
+ if (!node) {
1134
+ return null;
1135
+ }
1136
+ if (node.type === 'CallExpression') {
1137
+ return node.start;
1138
+ }
1139
+ if (node.type !== 'ExpressionStatement') {
1140
+ return null;
1141
+ }
1142
+ const { expression } = node;
1143
+ return expression?.type === 'CallExpression' ? expression.start : null;
1144
+ };
1145
+ // Statement forms that can directly contain a top-level contour call:
1146
+ // `core.contour(...)` as a bare statement,
1147
+ // `export const ... = core.contour(...)` (handled via VariableDeclarator),
1148
+ // `export default core.contour(...);`.
1149
+ const getCandidateCallHosts = (statement) => {
1150
+ if (statement.type !== 'ExportNamedDeclaration' &&
1151
+ statement.type !== 'ExportDefaultDeclaration') {
1152
+ return [statement];
1153
+ }
1154
+ const { declaration } = statement;
1155
+ return [statement, declaration];
1156
+ };
1157
+ const getTopLevelCallStartsFrom = (statement) => {
1158
+ const hosts = getCandidateCallHosts(statement);
1159
+ const starts = [];
1160
+ for (const host of hosts) {
1161
+ const start = getCallStartFromCandidate(host);
1162
+ if (start !== null) {
1163
+ starts.push(start);
1164
+ }
1165
+ }
1166
+ return starts;
1167
+ };
1168
+ /**
1169
+ * Collect the `start` offsets of `CallExpression` nodes that appear as
1170
+ * top-level `ExpressionStatement`s in a program body — including inside a
1171
+ * top-level `ExportNamedDeclaration` / `ExportDefaultDeclaration` wrapper.
1172
+ * Used to discriminate top-level statement-form calls from inline nested
1173
+ * calls when `topLevelOnly` is enabled.
1174
+ */
1175
+ const collectTopLevelStatementCallStarts = (ast) => {
1176
+ const body = ast.body ?? [];
1177
+ return new Set(body.flatMap(getTopLevelCallStartsFrom));
1178
+ };
1179
+ /**
1180
+ * Return every `contour('name', ...)` definition reachable from the AST, in
1181
+ * source order, deduplicated by call-expression start offset.
1182
+ *
1183
+ * Includes both top-level bindings (`const user = contour('user', ...)`) and
1184
+ * inline contour calls nested inside other expressions (e.g.
1185
+ * `contour('outer', { inner: contour('inner', ...).id() })`). Inline contours
1186
+ * carry no `bindingName` because they have no local binding — this asymmetry
1187
+ * is why {@link collectNamedContourIds} returns only the top-level subset
1188
+ * while {@link collectContourDefinitionIds} returns the full set.
1189
+ *
1190
+ * Pass `{ topLevelOnly: true }` via `options` to opt out of inline discovery
1191
+ * without disturbing callers that rely on the default behavior.
1192
+ *
1193
+ * @remarks
1194
+ * Supplying a pre-built `context` skips the second full-AST traversal inside
1195
+ * `buildFrameworkNamespaceContext` — useful for callers (such as
1196
+ * {@link collectContourReferenceSites}) that already built one.
1197
+ */
1198
+ export const findContourDefinitions = (ast, context, options) => {
1199
+ const definitions = [];
1200
+ const seenStarts = new Set();
1201
+ const resolvedContext = context ?? buildFrameworkNamespaceContext(ast);
1202
+ const topLevelOnly = options?.topLevelOnly === true;
1203
+ const addContourDefinition = (definition) => {
1204
+ if (seenStarts.has(definition.start)) {
1205
+ return;
1206
+ }
1207
+ definitions.push(definition);
1208
+ seenStarts.add(definition.start);
1209
+ };
1210
+ const addNamedContourDefinition = (id, init) => {
1211
+ if (!init) {
1212
+ return;
1213
+ }
1214
+ const definition = extractContourDefinition(init, resolvedContext);
1215
+ if (!definition) {
1216
+ return;
1217
+ }
1218
+ const bindingName = extractBindingName(id);
1219
+ if (bindingName) {
1220
+ addContourDefinition({ ...definition, bindingName });
1221
+ return;
1222
+ }
1223
+ addContourDefinition(definition);
1224
+ };
1225
+ // When `topLevelOnly` is set, collect the start offsets of call expressions
1226
+ // that sit directly in the program body as `ExpressionStatement`s (optionally
1227
+ // wrapped in `export`). These are top-level statement-form contour calls and
1228
+ // should still surface alongside `VariableDeclarator` bindings; only calls
1229
+ // nested inside other expressions are excluded.
1230
+ const topLevelStatementCallStarts = topLevelOnly
1231
+ ? collectTopLevelStatementCallStarts(ast)
1232
+ : null;
1233
+ walk(ast, (node) => {
1234
+ if (node.type === 'VariableDeclarator') {
1235
+ const { id, init } = node;
1236
+ addNamedContourDefinition(id, init);
1237
+ return;
1238
+ }
1239
+ if (topLevelStatementCallStarts &&
1240
+ !topLevelStatementCallStarts.has(node.start)) {
1241
+ return;
1242
+ }
1243
+ const definition = extractContourDefinition(node, resolvedContext);
1244
+ if (definition) {
1245
+ addContourDefinition(definition);
1246
+ }
1247
+ });
1248
+ return definitions.toSorted((left, right) => left.start - right.start);
1249
+ };
1250
+ /**
1251
+ * Collect the `name` of every contour definition in a parsed file, including
1252
+ * inline contours nested inside other expressions. Returns the same set of
1253
+ * names that {@link findContourDefinitions} discovers under default options.
1254
+ */
1255
+ export const collectContourDefinitionIds = (ast) => new Set(findContourDefinitions(ast).map((def) => def.name));
1256
+ /**
1257
+ * Collect the `localBinding → contourName` map for `const foo = contour(...)`
1258
+ * declarations. Inline contour calls are intentionally excluded because they
1259
+ * have no local binding — use {@link collectContourDefinitionIds} when the
1260
+ * full set of declared names is required.
1261
+ */
1262
+ export const collectNamedContourIds = (ast) => {
1263
+ const ids = new Map();
1264
+ for (const def of findContourDefinitions(ast)) {
1265
+ if (def.bindingName) {
1266
+ ids.set(def.bindingName, def.name);
1267
+ }
1268
+ }
1269
+ return ids;
1270
+ };
1271
+ const resolveNamedImportedName = (specifier, localName) => {
1272
+ const { imported } = specifier;
1273
+ const importedName = imported
1274
+ ? (identifierName(imported) ?? extractStringLiteral(imported))
1275
+ : null;
1276
+ return importedName ?? localName;
1277
+ };
1278
+ const extractImportSpecifierAlias = (specifier) => {
1279
+ if (specifier.type !== 'ImportSpecifier' &&
1280
+ specifier.type !== 'ImportDefaultSpecifier') {
1281
+ return null;
1282
+ }
1283
+ const { local } = specifier;
1284
+ const localName = identifierName(local);
1285
+ if (!localName) {
1286
+ return null;
1287
+ }
1288
+ // Default imports bind the default export of the source module to the local
1289
+ // name. We cannot statically recover the exported name without cross-file
1290
+ // analysis, so the local name is the best identifier we have for resolving
1291
+ // against `knownContourIds`. Treat the alias as an identity mapping; the
1292
+ // downstream resolver will fall through to `knownContourIds` on the binding
1293
+ // name and report it as missing when not found.
1294
+ if (specifier.type === 'ImportDefaultSpecifier') {
1295
+ return { importedName: localName, localName };
1296
+ }
1297
+ return {
1298
+ importedName: resolveNamedImportedName(specifier, localName),
1299
+ localName,
1300
+ };
1301
+ };
1302
+ /**
1303
+ * Collect `import { foo as bar } from '...'` and `import bar from '...'`
1304
+ * specifier mappings keyed by local binding name. The value is the original
1305
+ * exported name for named imports. Default imports map to themselves because
1306
+ * the exported name cannot be recovered statically — callers should fall
1307
+ * through to `knownContourIds` membership on the local binding name.
1308
+ */
1309
+ export const collectImportAliasMap = (ast) => {
1310
+ const aliases = new Map();
1311
+ walk(ast, (node) => {
1312
+ if (node.type !== 'ImportDeclaration') {
1313
+ return;
1314
+ }
1315
+ const specifiers = node['specifiers'] ?? [];
1316
+ for (const specifier of specifiers) {
1317
+ const alias = extractImportSpecifierAlias(specifier);
1318
+ if (alias) {
1319
+ aliases.set(alias.localName, alias.importedName);
1320
+ }
1321
+ }
1322
+ });
1323
+ return aliases;
1324
+ };
1325
+ const addUserNamespaceBindingsFromDeclaration = (node, into) => {
1326
+ if (isFrameworkNamespaceSource(getImportSourceValue(node))) {
1327
+ return;
1328
+ }
1329
+ const specifiers = node['specifiers'] ?? [];
1330
+ for (const specifier of specifiers) {
1331
+ if (specifier.type !== 'ImportNamespaceSpecifier') {
1332
+ continue;
1333
+ }
1334
+ const { local } = specifier;
1335
+ const localName = identifierName(local);
1336
+ if (localName) {
1337
+ into.add(localName);
1338
+ }
1339
+ }
1340
+ };
1341
+ /**
1342
+ * Collect local binding names introduced by `import * as <name> from '<src>'`
1343
+ * declarations whose source is NOT an `@ontrails/*` framework package. These
1344
+ * are user-defined namespace imports of contour modules (e.g. `import * as
1345
+ * contours from './contours'`), used to resolve `contours.user` member-access
1346
+ * references to contour ids.
1347
+ *
1348
+ * Framework namespace imports (`import * as core from '@ontrails/core'`) are
1349
+ * intentionally excluded — they carry framework primitives like
1350
+ * `core.contour(...)` and are resolved by {@link buildFrameworkNamespaceContext}.
1351
+ * Mixing them here would treat `core.contour` as a reference to a contour
1352
+ * named "contour", producing false positives.
1353
+ */
1354
+ export const collectUserNamespaceImportBindings = (ast) => {
1355
+ const bindings = new Set();
1356
+ walk(ast, (node) => {
1357
+ if (node.type !== 'ImportDeclaration') {
1358
+ return;
1359
+ }
1360
+ addUserNamespaceBindingsFromDeclaration(node, bindings);
1361
+ });
1362
+ return bindings;
1363
+ };
1364
+ /**
1365
+ * Walk the AST with a scope stack and collect `MemberExpression` start offsets
1366
+ * whose receiver is a user-namespace binding that is NOT shadowed by any
1367
+ * enclosing scope. Mirrors `collectFrameworkNamespacedCallStarts` for the
1368
+ * framework-namespace path so `contours.user` inside
1369
+ * `function f(contours) { ... }` is rejected as shadowed.
1370
+ */
1371
+ /**
1372
+ * Return the receiver-identifier name of a non-computed member access, or
1373
+ * `null` for any other node shape (computed access, non-member, etc.).
1374
+ */
1375
+ const getNonComputedMemberReceiver = (node) => {
1376
+ if (!isMemberAccessNonComputed(node)) {
1377
+ return null;
1378
+ }
1379
+ const { object } = node;
1380
+ return object ? identifierName(object) : null;
1381
+ };
1382
+ const collectUserNamespacedMemberStarts = (ast, bindings) => {
1383
+ const starts = new Set();
1384
+ if (bindings.size === 0) {
1385
+ return starts;
1386
+ }
1387
+ walkWithScopes(ast, (node, scopes) => {
1388
+ const receiver = getNonComputedMemberReceiver(node);
1389
+ if (!receiver || !bindings.has(receiver) || isShadowed(receiver, scopes)) {
1390
+ return;
1391
+ }
1392
+ starts.add(node.start);
1393
+ });
1394
+ return starts;
1395
+ };
1396
+ /**
1397
+ * Build a {@link UserNamespaceContext} for `ast`, including the scope-aware
1398
+ * `safeMemberStarts` gate. Prefer this over bare
1399
+ * {@link collectUserNamespaceImportBindings} so member access like
1400
+ * `contours.user` is rejected when `contours` is shadowed by a local binding.
1401
+ */
1402
+ export const buildUserNamespaceContext = (ast) => {
1403
+ const bindings = collectUserNamespaceImportBindings(ast);
1404
+ return {
1405
+ bindings,
1406
+ safeMemberStarts: collectUserNamespacedMemberStarts(ast, bindings),
1407
+ };
1408
+ };
1409
+ /**
1410
+ * Read a property key or member access identifier.
1411
+ *
1412
+ * Returns the identifier name for `Identifier` keys, or the underlying
1413
+ * string literal value for computed access via `['name']` / `"name"`.
1414
+ */
1415
+ export const getPropertyName = (node) => {
1416
+ if (typeof node !== 'object' || node === null) {
1417
+ return null;
1418
+ }
1419
+ const { name } = node;
1420
+ if (typeof name === 'string') {
1421
+ return name;
1422
+ }
1423
+ return isAstNode(node) ? extractStringLiteral(node) : null;
1424
+ };
1425
+ const stripContourSuffix = (name) => {
1426
+ const suffix = 'Contour';
1427
+ return name.endsWith(suffix) ? name.slice(0, -suffix.length) : name;
1428
+ };
1429
+ const resolveKnownContourName = (name, knownContourIds) => {
1430
+ if (knownContourIds?.has(name)) {
1431
+ return name;
1432
+ }
1433
+ // Support the common `const userContour = contour('user', ...)` naming
1434
+ // pattern when callers refer to the binding name instead of the contour ID.
1435
+ // Exact matches always win; suffix stripping is a fallback only.
1436
+ const stripped = stripContourSuffix(name);
1437
+ if (stripped !== name && knownContourIds?.has(stripped)) {
1438
+ return stripped;
1439
+ }
1440
+ return null;
1441
+ };
1442
+ /**
1443
+ * Resolve a local binding name to a contour ID, honoring import aliases.
1444
+ *
1445
+ * Strategies, in order:
1446
+ * 1. Local `const foo = contour('name', ...)` binding → the contour name.
1447
+ * 2. `knownContourIds` membership on the binding name itself (or the
1448
+ * conventional `Contour` suffix strip).
1449
+ * 3. `import { foo as bar }` → use the original exported name `foo`
1450
+ * (and apply strategy 2 / suffix-stripping against it so aliased imports
1451
+ * resolve correctly). If the imported name still isn't recognized, the
1452
+ * imported name is returned so the caller can report it missing.
1453
+ *
1454
+ * Returns `null` only when the name belongs to no known resolution path —
1455
+ * no local binding, no known contour ID, no import, and no suffix match.
1456
+ * Returning `null` means "this identifier is not a contour reference we can
1457
+ * reason about" (e.g. a bare undeclared variable), as opposed to
1458
+ * "a contour reference whose target is missing".
1459
+ */
1460
+ export const deriveContourIdentifierName = (bindingName, namedContourIds, knownContourIds, importAliases) => {
1461
+ const localName = namedContourIds.get(bindingName);
1462
+ if (localName) {
1463
+ return localName;
1464
+ }
1465
+ const known = resolveKnownContourName(bindingName, knownContourIds);
1466
+ if (known) {
1467
+ return known;
1468
+ }
1469
+ // If the binding came from an import, use the original exported name as
1470
+ // the resolution target. This lets `import { foo as bar }` resolve to
1471
+ // the exported `foo` rather than the local alias `bar`. If the imported
1472
+ // name still isn't recognized, return it so callers can report it as
1473
+ // missing under its original name.
1474
+ const importedName = importAliases?.get(bindingName);
1475
+ if (importedName) {
1476
+ return (resolveKnownContourName(importedName, knownContourIds) ?? importedName);
1477
+ }
1478
+ return null;
1479
+ };
1480
+ const getContourReferenceMember = (node) => {
1481
+ if (node.type !== 'MemberExpression' &&
1482
+ node.type !== 'StaticMemberExpression') {
1483
+ return null;
1484
+ }
1485
+ return node;
1486
+ };
1487
+ const asUserNamespaceContext = (input) => {
1488
+ if (!input) {
1489
+ return undefined;
1490
+ }
1491
+ return input instanceof Set
1492
+ ? { bindings: input }
1493
+ : input;
1494
+ };
1495
+ /**
1496
+ * Resolve a user-namespace member access like `contours.user` to its contour
1497
+ * id. Returns the property name (e.g. `'user'`) when the receiver identifier
1498
+ * is a known user-defined namespace binding AND — when the caller provides a
1499
+ * {@link UserNamespaceContext} with `safeMemberStarts` — the member access
1500
+ * site is in that set (i.e. the receiver is not shadowed by any enclosing
1501
+ * scope). Otherwise returns `null`.
1502
+ *
1503
+ * The property name is taken as the contour id verbatim — we cannot statically
1504
+ * resolve what `contours.user` binds to without reading the other file, so we
1505
+ * treat the member name as the candidate target and let
1506
+ * {@link deriveContourIdentifierName}'s downstream `knownContourIds` check
1507
+ * report a missing target.
1508
+ */
1509
+ export const isUserNamespaceReceiverAllowed = (receiver, memberStart, ctx) => {
1510
+ if (!ctx.bindings.has(receiver)) {
1511
+ return false;
1512
+ }
1513
+ // Scope-aware gate: when the pre-pass produced a set, the member access
1514
+ // must appear in it. Without the set, fall back to the bare name check.
1515
+ return ctx.safeMemberStarts ? ctx.safeMemberStarts.has(memberStart) : true;
1516
+ };
1517
+ const getContourReferenceTargetFromNamespaceMember = (member, userNamespace) => {
1518
+ const ctx = asUserNamespaceContext(userNamespace);
1519
+ if (!ctx || ctx.bindings.size === 0) {
1520
+ return null;
1521
+ }
1522
+ const receiver = member.object ? identifierName(member.object) : null;
1523
+ if (!receiver ||
1524
+ !isUserNamespaceReceiverAllowed(receiver, member.start, ctx)) {
1525
+ return null;
1526
+ }
1527
+ const { property } = member;
1528
+ if (!property || property.type !== 'Identifier') {
1529
+ return null;
1530
+ }
1531
+ return identifierName(property);
1532
+ };
1533
+ const getContourReferenceTargetFromObject = (object, namedContourIds, knownContourIds, importAliases, context, userNamespace) => {
1534
+ if (object.type === 'Identifier') {
1535
+ const bindingName = identifierName(object);
1536
+ return bindingName
1537
+ ? deriveContourIdentifierName(bindingName, namedContourIds, knownContourIds, importAliases)
1538
+ : null;
1539
+ }
1540
+ const member = getContourReferenceMember(object);
1541
+ if (member) {
1542
+ const namespaceTarget = getContourReferenceTargetFromNamespaceMember(member, userNamespace);
1543
+ if (namespaceTarget) {
1544
+ return namespaceTarget;
1545
+ }
1546
+ }
1547
+ return extractContourDefinition(object, context)?.name ?? null;
1548
+ };
1549
+ const CONTOUR_ID_WRAPPER_METHODS = new Set([
1550
+ 'brand',
1551
+ 'catch',
1552
+ 'default',
1553
+ 'describe',
1554
+ 'meta',
1555
+ 'nullable',
1556
+ 'nullish',
1557
+ 'optional',
1558
+ 'readonly',
1559
+ ]);
1560
+ const getContourIdCallMember = (node) => {
1561
+ const callee = node['callee'];
1562
+ const member = callee ? getContourReferenceMember(callee) : null;
1563
+ const propertyName = member ? identifierName(member.property) : null;
1564
+ return member && propertyName ? { member, propertyName } : null;
1565
+ };
1566
+ const getContourIdCallObject = function getContourIdCallObject(node) {
1567
+ const current = node;
1568
+ if (!current || current.type !== 'CallExpression') {
1569
+ return null;
1570
+ }
1571
+ const member = getContourIdCallMember(current);
1572
+ if (!member) {
1573
+ return null;
1574
+ }
1575
+ if (member.propertyName === 'id') {
1576
+ return member.member.object ?? null;
1577
+ }
1578
+ return CONTOUR_ID_WRAPPER_METHODS.has(member.propertyName)
1579
+ ? getContourIdCallObject(member.member.object)
1580
+ : null;
1581
+ };
1582
+ const extractContourReferenceTarget = (node, namedContourIds, knownContourIds, importAliases, context, userNamespace) => {
1583
+ const object = getContourIdCallObject(node);
1584
+ return object
1585
+ ? getContourReferenceTargetFromObject(object, namedContourIds, knownContourIds, importAliases, context, userNamespace)
1586
+ : null;
1587
+ };
1588
+ const getContourShapeProperties = (definition) => definition.shape['properties'] ?? [];
1589
+ const buildContourReferenceSite = (definition, property, namedContourIds, knownContourIds, importAliases, context, userNamespace) => {
1590
+ if (property.type !== 'Property') {
1591
+ return null;
1592
+ }
1593
+ const field = getPropertyName(property.key);
1594
+ const target = extractContourReferenceTarget(property.value, namedContourIds, knownContourIds, importAliases, context, userNamespace);
1595
+ if (!field || !target) {
1596
+ return null;
1597
+ }
1598
+ return {
1599
+ field,
1600
+ source: definition.name,
1601
+ start: property.start,
1602
+ target,
1603
+ };
1604
+ };
1605
+ const findContourReferenceSitesForDefinition = (definition, namedContourIds, knownContourIds, importAliases, context, userNamespace) => getContourShapeProperties(definition).flatMap((property) => {
1606
+ const reference = buildContourReferenceSite(definition, property, namedContourIds, knownContourIds, importAliases, context, userNamespace);
1607
+ return reference ? [reference] : [];
1608
+ });
1609
+ /** Collect all contour field references declared via `.id()` in a parsed file. */
1610
+ export const collectContourReferenceSites = (ast, knownContourIds) => {
1611
+ const namedContourIds = collectNamedContourIds(ast);
1612
+ const importAliases = collectImportAliasMap(ast);
1613
+ const userNamespace = buildUserNamespaceContext(ast);
1614
+ const context = buildFrameworkNamespaceContext(ast);
1615
+ return findContourDefinitions(ast, context).flatMap((definition) => findContourReferenceSitesForDefinition(definition, namedContourIds, knownContourIds, importAliases, context, userNamespace));
1616
+ };
1617
+ /** Collect contour reference targets keyed by source contour name. */
1618
+ export const collectContourReferenceTargetsByName = (ast, knownContourIds) => {
1619
+ const targetsByName = new Map();
1620
+ for (const reference of collectContourReferenceSites(ast, knownContourIds)) {
1621
+ const existing = targetsByName.get(reference.source);
1622
+ if (existing) {
1623
+ existing.add(reference.target);
1624
+ continue;
1625
+ }
1626
+ targetsByName.set(reference.source, new Set([reference.target]));
1627
+ }
1628
+ return new Map([...targetsByName.entries()].map(([name, targets]) => [name, [...targets]]));
1629
+ };
1630
+ // ---------------------------------------------------------------------------
1631
+ // Blaze body extraction
1632
+ // ---------------------------------------------------------------------------
1633
+ /**
1634
+ * Extract top-level `blaze:` property values from an ObjectExpression's direct properties.
1635
+ *
1636
+ * Does not recurse into nested objects, so `meta: { blaze: ... }` is ignored.
1637
+ */
1638
+ const extractBlazeFromConfig = (config) => {
1639
+ const bodies = [];
1640
+ const properties = config['properties'];
1641
+ if (!properties) {
1642
+ return bodies;
1643
+ }
1644
+ for (const prop of properties) {
1645
+ if (prop.type === 'Property' &&
1646
+ prop.key?.name === 'blaze' &&
1647
+ isAstNode(prop.value)) {
1648
+ bodies.push(prop.value);
1649
+ }
1650
+ }
1651
+ return bodies;
1652
+ };
1653
+ /**
1654
+ * Find `blaze:` property values.
1655
+ *
1656
+ * When given an ObjectExpression (trail config), returns only its direct `blaze:`
1657
+ * properties. When given a full AST, finds trail definitions first and extracts
1658
+ * `blaze:` from each config — in both cases ignoring nested `blaze:` properties
1659
+ * (e.g. `meta: { blaze: ... }`).
1660
+ */
1661
+ export const findBlazeBodies = (node) => {
1662
+ if (node.type === 'ObjectExpression') {
1663
+ return extractBlazeFromConfig(node);
1664
+ }
1665
+ // Full AST — find trail definitions and extract blaze from their configs
1666
+ const bodies = [];
1667
+ for (const def of findTrailDefinitions(node)) {
1668
+ bodies.push(...extractBlazeFromConfig(def.config));
1669
+ }
1670
+ return bodies;
1671
+ };
1672
+ /**
1673
+ * Collect all `signal('id', { ... })` / `signal({ id: 'x', ... })` definition IDs.
1674
+ *
1675
+ * Uses `findTrailDefinitions` under the hood — it already recognizes both
1676
+ * `trail` and `signal` call sites, distinguished by the `kind` field.
1677
+ */
1678
+ export const collectSignalDefinitionIds = (ast) => {
1679
+ const ids = new Set();
1680
+ for (const def of findTrailDefinitions(ast)) {
1681
+ if (def.kind === 'signal') {
1682
+ ids.add(def.id);
1683
+ }
1684
+ }
1685
+ return ids;
1686
+ };
1687
+ /** Collect `const foo = trail('id', ...)` bindings from a parsed file. */
1688
+ export const collectNamedTrailIds = (ast) => {
1689
+ const ids = new Map();
1690
+ const context = buildFrameworkNamespaceContext(ast);
1691
+ walk(ast, (node) => {
1692
+ if (node.type !== 'VariableDeclarator') {
1693
+ return;
1694
+ }
1695
+ const { id, init } = node;
1696
+ if (!init) {
1697
+ return;
1698
+ }
1699
+ const def = extractTrailDefinition(init, context);
1700
+ const name = extractBindingName(id);
1701
+ if (def?.kind === 'trail' && name) {
1702
+ ids.set(name, def.id);
1703
+ }
1704
+ });
1705
+ return ids;
1706
+ };
1707
+ /** Extract the raw `crosses: [...]` array elements from a trail config. */
1708
+ export const getCrossElements = (config) => {
1709
+ const crossesProp = findConfigProperty(config, 'crosses');
1710
+ if (!crossesProp) {
1711
+ return [];
1712
+ }
1713
+ const arrayNode = crossesProp.value;
1714
+ if (!arrayNode || arrayNode.type !== 'ArrayExpression') {
1715
+ return [];
1716
+ }
1717
+ const elements = arrayNode['elements'];
1718
+ return elements ?? [];
1719
+ };
1720
+ /**
1721
+ * Resolve a single `crosses: [...]` element to its target trail ID.
1722
+ *
1723
+ * Handles string literals, identifier references (via `namedTrailIds` map or
1724
+ * `const NAME = '...'` resolution), and inline `trail(...)` call expressions.
1725
+ */
1726
+ export const deriveCrossElementId = (element, sourceCode, namedTrailIds) => {
1727
+ if (isStringLiteral(element)) {
1728
+ return getStringValue(element);
1729
+ }
1730
+ if (element.type === 'Identifier') {
1731
+ const name = identifierName(element);
1732
+ return name
1733
+ ? (namedTrailIds.get(name) ?? deriveConstString(name, sourceCode))
1734
+ : null;
1735
+ }
1736
+ const inlineDef = extractTrailDefinition(element);
1737
+ return inlineDef?.kind === 'trail' ? inlineDef.id : null;
1738
+ };
1739
+ /**
1740
+ * Collect all trail IDs referenced by a single trail definition's
1741
+ * `crosses: [...]` array, deduplicated.
1742
+ */
1743
+ export const extractDefinitionCrossTargetIds = (config, sourceCode, namedTrailIds) => [
1744
+ ...new Set(getCrossElements(config).flatMap((element) => {
1745
+ const id = deriveCrossElementId(element, sourceCode, namedTrailIds);
1746
+ return id ? [id] : [];
1747
+ })),
1748
+ ];
1749
+ /** Collect all trail IDs referenced by declared `crosses: [...]` arrays. */
1750
+ export const collectCrossTargetTrailIds = (ast, sourceCode) => {
1751
+ const ids = new Set();
1752
+ const namedTrailIds = collectNamedTrailIds(ast);
1753
+ for (const def of findTrailDefinitions(ast)) {
1754
+ if (def.kind !== 'trail') {
1755
+ continue;
1756
+ }
1757
+ for (const id of extractDefinitionCrossTargetIds(def.config, sourceCode, namedTrailIds)) {
1758
+ ids.add(id);
1759
+ }
1760
+ }
1761
+ return ids;
1762
+ };
1763
+ const extractTrailIntent = (config) => {
1764
+ const intentProp = findConfigProperty(config, 'intent');
1765
+ if (!intentProp || !isStringLiteral(intentProp.value)) {
1766
+ return 'write';
1767
+ }
1768
+ const value = getStringValue(intentProp.value);
1769
+ return value === 'destroy' || value === 'read' ? value : 'write';
1770
+ };
1771
+ /** Collect the normalized intent for every trail definition in a parsed file. */
1772
+ export const collectTrailIntentsById = (ast) => {
1773
+ const intents = new Map();
1774
+ for (const def of findTrailDefinitions(ast)) {
1775
+ if (def.kind === 'trail') {
1776
+ intents.set(def.id, extractTrailIntent(def.config));
1777
+ }
1778
+ }
1779
+ return intents;
1780
+ };
1781
+ /**
1782
+ * Build a composite key for a store table: `${storeBinding}:${tableName}`,
1783
+ * falling back to the bare `tableName` when the enclosing store has no local
1784
+ * binding. Centralized so rule keying stays stable.
1785
+ *
1786
+ * @remarks
1787
+ * The key is intentionally file-local (no module path prefix). Cross-file
1788
+ * aggregation in `ProjectContext` merges keys from all files, so two files
1789
+ * with `const db = store({ notes: ... })` both produce `db:notes` — this is
1790
+ * the desired behavior because the warden checks for *pattern completeness*
1791
+ * across the project and matching keys signals that the same logical table
1792
+ * is covered. If two genuinely different tables share a binding and name,
1793
+ * that is a code-level naming collision the developer should resolve.
1794
+ */
1795
+ export const makeStoreTableKey = (storeBinding, tableName) => (storeBinding ? `${storeBinding}:${tableName}` : tableName);
1796
+ const isBooleanLiteral = (node) => Boolean(node &&
1797
+ ((node.type === 'BooleanLiteral' &&
1798
+ node.value === true) ||
1799
+ (node.type === 'Literal' &&
1800
+ node.value === true)));
1801
+ /**
1802
+ * Check if a node is a `CallExpression` to the identifier `name`.
1803
+ *
1804
+ * e.g. `isNamedCall(node, 'store')` matches `store({...})` but not
1805
+ * `someObj.store()` or `storeAlt()`.
1806
+ */
1807
+ export const isNamedCall = (node, name) => !!node &&
1808
+ node.type === 'CallExpression' &&
1809
+ identifierName(node.callee) === name;
1810
+ /**
1811
+ * Narrow a member-expression node (`a.b` or `a['b']`) to its `object` /
1812
+ * `property` pair, returning `null` for anything else.
1813
+ */
1814
+ export const getMemberExpression = (node) => {
1815
+ if (!node ||
1816
+ (node.type !== 'MemberExpression' && node.type !== 'StaticMemberExpression')) {
1817
+ return null;
1818
+ }
1819
+ return node;
1820
+ };
1821
+ /**
1822
+ * Resolve a `<store>.tables.<name>` member expression to its store binding
1823
+ * and table name.
1824
+ *
1825
+ * Returns `null` for anything that isn't a two-level member access ending in
1826
+ * `.tables.<name>`. The store binding is the identifier of the object owning
1827
+ * `.tables` — typically the local binding from `const db = store(...)`.
1828
+ */
1829
+ export const extractStoreTableFromMember = (node) => {
1830
+ const member = getMemberExpression(node);
1831
+ const tableName = member ? getPropertyName(member.property) : null;
1832
+ const tablesMember = member ? getMemberExpression(member.object) : null;
1833
+ if (!tableName || !tablesMember) {
1834
+ return null;
1835
+ }
1836
+ if (getPropertyName(tablesMember.property) !== 'tables') {
1837
+ return null;
1838
+ }
1839
+ const storeBinding = identifierName(tablesMember.object) ?? null;
1840
+ return { storeBinding, tableName };
1841
+ };
1842
+ /**
1843
+ * Back-compat shim for rule code that only needs the table name. Prefer
1844
+ * `extractStoreTableFromMember` so the caller can build a composite key.
1845
+ */
1846
+ export const extractStoreTableIdFromMember = (node) => extractStoreTableFromMember(node)?.tableName ?? null;
1847
+ /**
1848
+ * Collect `const foo = <store>.tables.<name>` bindings from a parsed file,
1849
+ * keyed by the local binding name. Values are the composite table key
1850
+ * (`${storeBinding}:${tableName}`) so callers can dedupe across stores that
1851
+ * share a table name.
1852
+ */
1853
+ export const collectNamedStoreTableIds = (ast) => {
1854
+ const ids = new Map();
1855
+ walk(ast, (node) => {
1856
+ if (node.type !== 'VariableDeclarator') {
1857
+ return;
1858
+ }
1859
+ const { id, init } = node;
1860
+ const name = extractBindingName(id);
1861
+ const table = extractStoreTableFromMember(init);
1862
+ if (name && table) {
1863
+ ids.set(name, makeStoreTableKey(table.storeBinding, table.tableName));
1864
+ }
1865
+ });
1866
+ return ids;
1867
+ };
1868
+ /**
1869
+ * Resolve an argument node to a composite store-table key
1870
+ * (`${storeBinding}:${tableName}` or bare `tableName` when anonymous).
1871
+ *
1872
+ * Handles the two authoring patterns:
1873
+ * - direct member access: `db.tables.notes`
1874
+ * - identifier reference: `const notesTable = db.tables.notes; crud(notesTable, …)`
1875
+ */
1876
+ export const deriveStoreTableId = (node, namedStoreTableIds) => {
1877
+ if (!node) {
1878
+ return null;
1879
+ }
1880
+ if (node.type === 'Identifier') {
1881
+ const name = identifierName(node);
1882
+ return name ? (namedStoreTableIds.get(name) ?? null) : null;
1883
+ }
1884
+ const member = extractStoreTableFromMember(node);
1885
+ return member
1886
+ ? makeStoreTableKey(member.storeBinding, member.tableName)
1887
+ : null;
1888
+ };
1889
+ const extractStoreTableDefinitions = (node, storeBinding) => {
1890
+ if (!isNamedCall(node, 'store')) {
1891
+ return [];
1892
+ }
1893
+ const [tablesArg] = (node
1894
+ .arguments ?? []);
1895
+ if (!tablesArg || tablesArg.type !== 'ObjectExpression') {
1896
+ return [];
1897
+ }
1898
+ const properties = tablesArg['properties'];
1899
+ if (!properties) {
1900
+ return [];
1901
+ }
1902
+ return properties.flatMap((property) => {
1903
+ if (property.type !== 'Property') {
1904
+ return [];
1905
+ }
1906
+ const name = getPropertyName(property.key);
1907
+ const value = property.value;
1908
+ if (!name || value?.type !== 'ObjectExpression') {
1909
+ return [];
1910
+ }
1911
+ const versionedProp = findConfigProperty(value, 'versioned');
1912
+ return [
1913
+ {
1914
+ key: makeStoreTableKey(storeBinding, name),
1915
+ name,
1916
+ start: property.start,
1917
+ storeBinding,
1918
+ versioned: isBooleanLiteral(versionedProp?.value),
1919
+ },
1920
+ ];
1921
+ });
1922
+ };
1923
+ export const findStoreTableDefinitions = (ast) => {
1924
+ const definitions = [];
1925
+ const seenStoreCalls = new WeakSet();
1926
+ // First pass: bound stores (walk VariableDeclarators so we know the binding).
1927
+ walk(ast, (node) => {
1928
+ if (node.type !== 'VariableDeclarator') {
1929
+ return;
1930
+ }
1931
+ const { id, init } = node;
1932
+ if (!init || !isNamedCall(init, 'store')) {
1933
+ return;
1934
+ }
1935
+ seenStoreCalls.add(init);
1936
+ const storeBinding = extractBindingName(id);
1937
+ definitions.push(...extractStoreTableDefinitions(init, storeBinding));
1938
+ });
1939
+ // Second pass: anonymous `store({...})` calls not bound to a variable
1940
+ // (e.g. an inline default export). Use the bare table name as the key.
1941
+ walk(ast, (node) => {
1942
+ if (!isNamedCall(node, 'store') || seenStoreCalls.has(node)) {
1943
+ return;
1944
+ }
1945
+ definitions.push(...extractStoreTableDefinitions(node, null));
1946
+ });
1947
+ return definitions;
1948
+ };
1949
+ export const collectVersionedStoreTableIds = (ast) => new Set(findStoreTableDefinitions(ast).flatMap((definition) => definition.versioned ? [definition.key] : []));
1950
+ export const collectCrudTableIds = (ast) => {
1951
+ const ids = new Set();
1952
+ const namedStoreTableIds = collectNamedStoreTableIds(ast);
1953
+ walk(ast, (node) => {
1954
+ if (!isNamedCall(node, 'crud')) {
1955
+ return;
1956
+ }
1957
+ const [tableArg] = (node
1958
+ .arguments ?? []);
1959
+ const tableId = deriveStoreTableId(tableArg, namedStoreTableIds);
1960
+ if (tableId) {
1961
+ ids.add(tableId);
1962
+ }
1963
+ });
1964
+ return ids;
1965
+ };
1966
+ export const collectReconcileTableIds = (ast) => {
1967
+ const ids = new Set();
1968
+ const namedStoreTableIds = collectNamedStoreTableIds(ast);
1969
+ walk(ast, (node) => {
1970
+ if (!isNamedCall(node, 'reconcile')) {
1971
+ return;
1972
+ }
1973
+ const [configArg] = (node.arguments ?? []);
1974
+ if (!configArg || configArg.type !== 'ObjectExpression') {
1975
+ return;
1976
+ }
1977
+ const tableProp = findConfigProperty(configArg, 'table');
1978
+ const tableId = deriveStoreTableId(tableProp?.value, namedStoreTableIds);
1979
+ if (tableId) {
1980
+ ids.add(tableId);
1981
+ }
1982
+ });
1983
+ return ids;
1984
+ };
1985
+ const STORE_SIGNAL_OPERATIONS = new Set(['created', 'removed', 'updated']);
1986
+ const extractStoreSignalIdFromMember = (node, namedStoreTableIds) => {
1987
+ const member = getMemberExpression(node);
1988
+ const operation = member ? getPropertyName(member.property) : null;
1989
+ if (!operation || !STORE_SIGNAL_OPERATIONS.has(operation)) {
1990
+ return null;
1991
+ }
1992
+ const signalsMember = member ? getMemberExpression(member.object) : null;
1993
+ if (!signalsMember || getPropertyName(signalsMember.property) !== 'signals') {
1994
+ return null;
1995
+ }
1996
+ const tableId = deriveStoreTableId(signalsMember.object, namedStoreTableIds);
1997
+ return tableId ? `${tableId}.${operation}` : null;
1998
+ };
1999
+ const collectNamedStoreSignalIds = (ast, namedStoreTableIds) => {
2000
+ const ids = new Map();
2001
+ walk(ast, (node) => {
2002
+ if (node.type !== 'VariableDeclarator') {
2003
+ return;
2004
+ }
2005
+ const { id, init } = node;
2006
+ const name = extractBindingName(id);
2007
+ const signalId = extractStoreSignalIdFromMember(init, namedStoreTableIds);
2008
+ if (name && signalId) {
2009
+ ids.set(name, signalId);
2010
+ }
2011
+ });
2012
+ return ids;
2013
+ };
2014
+ const getOnElements = (config) => {
2015
+ const onProp = findConfigProperty(config, 'on');
2016
+ if (!onProp) {
2017
+ return [];
2018
+ }
2019
+ const arrayNode = onProp.value;
2020
+ if (!arrayNode || arrayNode.type !== 'ArrayExpression') {
2021
+ return [];
2022
+ }
2023
+ const elements = arrayNode['elements'];
2024
+ return elements ?? [];
2025
+ };
2026
+ const resolveNamedOnSignalId = (element, sourceCode, namedStoreSignalIds) => {
2027
+ if (element.type !== 'Identifier') {
2028
+ return null;
2029
+ }
2030
+ const name = identifierName(element);
2031
+ return name
2032
+ ? (namedStoreSignalIds.get(name) ?? deriveConstString(name, sourceCode))
2033
+ : null;
2034
+ };
2035
+ const resolveInlineOnSignalId = (element) => {
2036
+ const definition = extractTrailDefinition(element);
2037
+ return definition?.kind === 'signal' ? definition.id : null;
2038
+ };
2039
+ const resolveOnElementSignalId = (element, sourceCode, namedStoreSignalIds, namedStoreTableIds) => {
2040
+ if (isStringLiteral(element)) {
2041
+ return getStringValue(element);
2042
+ }
2043
+ return (extractStoreSignalIdFromMember(element, namedStoreTableIds) ??
2044
+ resolveNamedOnSignalId(element, sourceCode, namedStoreSignalIds) ??
2045
+ resolveInlineOnSignalId(element));
2046
+ };
2047
+ const addOnTargetSignalIds = (config, ids, sourceCode, namedStoreSignalIds, namedStoreTableIds) => {
2048
+ for (const element of getOnElements(config)) {
2049
+ const signalId = resolveOnElementSignalId(element, sourceCode, namedStoreSignalIds, namedStoreTableIds);
2050
+ if (signalId) {
2051
+ ids.add(signalId);
2052
+ }
2053
+ }
2054
+ };
2055
+ export const collectOnTargetSignalIds = (ast, sourceCode) => {
2056
+ const ids = new Set();
2057
+ const namedStoreTableIds = collectNamedStoreTableIds(ast);
2058
+ const namedStoreSignalIds = collectNamedStoreSignalIds(ast, namedStoreTableIds);
2059
+ for (const definition of findTrailDefinitions(ast)) {
2060
+ if (definition.kind === 'trail') {
2061
+ addOnTargetSignalIds(definition.config, ids, sourceCode, namedStoreSignalIds, namedStoreTableIds);
2062
+ }
2063
+ }
2064
+ return ids;
320
2065
  };
321
2066
  // ---------------------------------------------------------------------------
322
2067
  // Misc helpers