@ontrails/warden 1.0.0-beta.14 → 1.0.0-beta.16

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 (351) hide show
  1. package/CHANGELOG.md +149 -0
  2. package/README.md +77 -34
  3. package/bin/warden.ts +22 -0
  4. package/package.json +26 -6
  5. package/src/ast.ts +28 -0
  6. package/src/cli.ts +1169 -116
  7. package/src/command.ts +927 -0
  8. package/src/config.ts +184 -0
  9. package/src/drift.ts +76 -34
  10. package/src/formatters.ts +54 -7
  11. package/src/guide.ts +234 -0
  12. package/src/index.ts +151 -38
  13. package/src/project-context.ts +163 -0
  14. package/src/resolve.ts +530 -0
  15. package/src/rules/activation-orphan.ts +97 -0
  16. package/src/rules/ast.ts +2884 -124
  17. package/src/rules/circular-refs.ts +154 -0
  18. package/src/rules/{context-no-trailhead-types.ts → context-no-surface-types.ts} +72 -12
  19. package/src/rules/contour-exists.ts +251 -0
  20. package/src/rules/contour-ids.ts +15 -0
  21. package/src/rules/cross-declarations.ts +277 -69
  22. package/src/rules/dead-internal-trail.ts +141 -0
  23. package/src/rules/draft-file-marking.ts +51 -3
  24. package/src/rules/draft-visible-debt.ts +40 -15
  25. package/src/rules/error-mapping-completeness.ts +288 -0
  26. package/src/rules/example-valid.ts +401 -0
  27. package/src/rules/fires-declarations.ts +758 -0
  28. package/src/rules/implementation-returns-result.ts +1042 -122
  29. package/src/rules/incomplete-accessor-for-standard-op.ts +272 -0
  30. package/src/rules/incomplete-crud.ts +580 -0
  31. package/src/rules/index.ts +156 -19
  32. package/src/rules/intent-propagation.ts +127 -0
  33. package/src/rules/layer-field-name-drift.ts +96 -0
  34. package/src/rules/metadata.ts +508 -0
  35. package/src/rules/missing-reconcile.ts +98 -0
  36. package/src/rules/missing-visibility.ts +110 -0
  37. package/src/rules/no-dev-permit-in-source.ts +99 -0
  38. package/src/rules/no-direct-implementation-call.ts +1 -1
  39. package/src/rules/no-legacy-layer-imports.ts +193 -0
  40. package/src/rules/no-native-error-result.ts +111 -0
  41. package/src/rules/no-sync-result-assumption.ts +1131 -96
  42. package/src/rules/no-throw-in-detour-recover.ts +225 -0
  43. package/src/rules/no-throw-in-implementation.ts +6 -4
  44. package/src/rules/on-references-exist.ts +194 -0
  45. package/src/rules/orphaned-signal.ts +150 -0
  46. package/src/rules/owner-projection-parity.ts +146 -0
  47. package/src/rules/permit-governance.ts +25 -0
  48. package/src/rules/public-internal-deep-imports.ts +517 -0
  49. package/src/rules/public-output-schema.ts +29 -0
  50. package/src/rules/public-union-output-discriminants.ts +150 -0
  51. package/src/rules/read-intent-fires.ts +187 -0
  52. package/src/rules/reference-exists.ts +98 -0
  53. package/src/rules/registry-names.ts +113 -0
  54. package/src/rules/resolved-import-boundary.ts +146 -0
  55. package/src/rules/{provision-declarations.ts → resource-declarations.ts} +208 -138
  56. package/src/rules/{provision-exists.ts → resource-exists.ts} +46 -51
  57. package/src/rules/resource-id-grammar.ts +65 -0
  58. package/src/rules/scan.ts +0 -26
  59. package/src/rules/scheduled-destroy-intent.ts +44 -0
  60. package/src/rules/signal-graph-coaching.ts +191 -0
  61. package/src/rules/specs.ts +5 -1
  62. package/src/rules/static-resource-accessor-preference.ts +657 -0
  63. package/src/rules/types.ts +187 -7
  64. package/src/rules/unmaterialized-activation-source.ts +84 -0
  65. package/src/rules/unreachable-detour-shadowing.ts +344 -0
  66. package/src/rules/valid-describe-refs.ts +160 -32
  67. package/src/rules/valid-detour-contract.ts +78 -0
  68. package/src/rules/warden-export-symmetry.ts +533 -0
  69. package/src/rules/warden-rules-use-ast.ts +996 -0
  70. package/src/rules/webhook-route-collision.ts +243 -0
  71. package/src/trails/activation-orphan.trail.ts +81 -0
  72. package/src/trails/circular-refs.trail.ts +29 -0
  73. package/src/trails/{context-no-trailhead-types.trail.ts → context-no-surface-types.trail.ts} +4 -4
  74. package/src/trails/contour-exists.trail.ts +21 -0
  75. package/src/trails/dead-internal-trail.trail.ts +26 -0
  76. package/src/trails/draft-file-marking.trail.ts +16 -0
  77. package/src/trails/draft-visible-debt.trail.ts +16 -0
  78. package/src/trails/error-mapping-completeness.trail.ts +29 -0
  79. package/src/trails/example-valid.trail.ts +25 -0
  80. package/src/trails/fires-declarations.trail.ts +23 -0
  81. package/src/trails/incomplete-accessor-for-standard-op.trail.ts +76 -0
  82. package/src/trails/incomplete-crud.trail.ts +39 -0
  83. package/src/trails/index.ts +56 -8
  84. package/src/trails/intent-propagation.trail.ts +30 -0
  85. package/src/trails/layer-field-name-drift.trail.ts +39 -0
  86. package/src/trails/missing-reconcile.trail.ts +33 -0
  87. package/src/trails/missing-visibility.trail.ts +22 -0
  88. package/src/trails/no-dev-permit-in-source.trail.ts +16 -0
  89. package/src/trails/no-legacy-layer-imports.trail.ts +35 -0
  90. package/src/trails/no-native-error-result.trail.ts +18 -0
  91. package/src/trails/no-throw-in-detour-recover.trail.ts +24 -0
  92. package/src/trails/on-references-exist.trail.ts +21 -0
  93. package/src/trails/orphaned-signal.trail.ts +36 -0
  94. package/src/trails/owner-projection-parity.trail.ts +26 -0
  95. package/src/trails/permit-governance.trail.ts +51 -0
  96. package/src/trails/public-internal-deep-imports.trail.ts +94 -0
  97. package/src/trails/public-output-schema.trail.ts +55 -0
  98. package/src/trails/public-union-output-discriminants.trail.ts +33 -0
  99. package/src/trails/read-intent-fires.trail.ts +20 -0
  100. package/src/trails/reference-exists.trail.ts +25 -0
  101. package/src/trails/resolved-import-boundary.trail.ts +109 -0
  102. package/src/trails/{provision-declarations.trail.ts → resource-declarations.trail.ts} +6 -6
  103. package/src/trails/{provision-exists.trail.ts → resource-exists.trail.ts} +7 -7
  104. package/src/trails/resource-id-grammar.trail.ts +39 -0
  105. package/src/trails/run.ts +133 -24
  106. package/src/trails/scheduled-destroy-intent.trail.ts +56 -0
  107. package/src/trails/schema.ts +122 -4
  108. package/src/trails/signal-graph-coaching.trail.ts +74 -0
  109. package/src/trails/static-resource-accessor-preference.trail.ts +25 -0
  110. package/src/trails/unmaterialized-activation-source.trail.ts +69 -0
  111. package/src/trails/unreachable-detour-shadowing.trail.ts +45 -0
  112. package/src/trails/valid-detour-contract.trail.ts +71 -0
  113. package/src/trails/warden-export-symmetry.trail.ts +16 -0
  114. package/src/trails/warden-rules-use-ast.trail.ts +45 -0
  115. package/src/trails/webhook-route-collision.trail.ts +50 -0
  116. package/src/trails/wrap-rule.ts +133 -14
  117. package/src/workspaces.ts +238 -0
  118. package/.turbo/turbo-build.log +0 -1
  119. package/.turbo/turbo-lint.log +0 -3
  120. package/.turbo/turbo-typecheck.log +0 -1
  121. package/dist/cli.d.ts +0 -46
  122. package/dist/cli.d.ts.map +0 -1
  123. package/dist/cli.js +0 -247
  124. package/dist/cli.js.map +0 -1
  125. package/dist/draft.d.ts +0 -5
  126. package/dist/draft.d.ts.map +0 -1
  127. package/dist/draft.js +0 -16
  128. package/dist/draft.js.map +0 -1
  129. package/dist/drift.d.ts +0 -29
  130. package/dist/drift.d.ts.map +0 -1
  131. package/dist/drift.js +0 -61
  132. package/dist/drift.js.map +0 -1
  133. package/dist/formatters.d.ts +0 -30
  134. package/dist/formatters.d.ts.map +0 -1
  135. package/dist/formatters.js +0 -98
  136. package/dist/formatters.js.map +0 -1
  137. package/dist/index.d.ts +0 -37
  138. package/dist/index.d.ts.map +0 -1
  139. package/dist/index.js +0 -38
  140. package/dist/index.js.map +0 -1
  141. package/dist/rules/ast.d.ts +0 -82
  142. package/dist/rules/ast.d.ts.map +0 -1
  143. package/dist/rules/ast.js +0 -363
  144. package/dist/rules/ast.js.map +0 -1
  145. package/dist/rules/context-no-surface-types.d.ts +0 -12
  146. package/dist/rules/context-no-surface-types.d.ts.map +0 -1
  147. package/dist/rules/context-no-surface-types.js +0 -96
  148. package/dist/rules/context-no-surface-types.js.map +0 -1
  149. package/dist/rules/context-no-trailhead-types.d.ts +0 -12
  150. package/dist/rules/context-no-trailhead-types.d.ts.map +0 -1
  151. package/dist/rules/context-no-trailhead-types.js +0 -96
  152. package/dist/rules/context-no-trailhead-types.js.map +0 -1
  153. package/dist/rules/cross-declarations.d.ts +0 -13
  154. package/dist/rules/cross-declarations.d.ts.map +0 -1
  155. package/dist/rules/cross-declarations.js +0 -264
  156. package/dist/rules/cross-declarations.js.map +0 -1
  157. package/dist/rules/draft-file-marking.d.ts +0 -6
  158. package/dist/rules/draft-file-marking.d.ts.map +0 -1
  159. package/dist/rules/draft-file-marking.js +0 -65
  160. package/dist/rules/draft-file-marking.js.map +0 -1
  161. package/dist/rules/draft-visible-debt.d.ts +0 -12
  162. package/dist/rules/draft-visible-debt.d.ts.map +0 -1
  163. package/dist/rules/draft-visible-debt.js +0 -45
  164. package/dist/rules/draft-visible-debt.js.map +0 -1
  165. package/dist/rules/follow-declarations.d.ts +0 -13
  166. package/dist/rules/follow-declarations.d.ts.map +0 -1
  167. package/dist/rules/follow-declarations.js +0 -264
  168. package/dist/rules/follow-declarations.js.map +0 -1
  169. package/dist/rules/implementation-returns-result.d.ts +0 -13
  170. package/dist/rules/implementation-returns-result.d.ts.map +0 -1
  171. package/dist/rules/implementation-returns-result.js +0 -277
  172. package/dist/rules/implementation-returns-result.js.map +0 -1
  173. package/dist/rules/index.d.ts +0 -20
  174. package/dist/rules/index.d.ts.map +0 -1
  175. package/dist/rules/index.js +0 -49
  176. package/dist/rules/index.js.map +0 -1
  177. package/dist/rules/no-direct-impl-in-route.d.ts +0 -12
  178. package/dist/rules/no-direct-impl-in-route.d.ts.map +0 -1
  179. package/dist/rules/no-direct-impl-in-route.js +0 -47
  180. package/dist/rules/no-direct-impl-in-route.js.map +0 -1
  181. package/dist/rules/no-direct-implementation-call.d.ts +0 -12
  182. package/dist/rules/no-direct-implementation-call.d.ts.map +0 -1
  183. package/dist/rules/no-direct-implementation-call.js +0 -39
  184. package/dist/rules/no-direct-implementation-call.js.map +0 -1
  185. package/dist/rules/no-sync-result-assumption.d.ts +0 -6
  186. package/dist/rules/no-sync-result-assumption.d.ts.map +0 -1
  187. package/dist/rules/no-sync-result-assumption.js +0 -98
  188. package/dist/rules/no-sync-result-assumption.js.map +0 -1
  189. package/dist/rules/no-throw-in-detour-target.d.ts +0 -12
  190. package/dist/rules/no-throw-in-detour-target.d.ts.map +0 -1
  191. package/dist/rules/no-throw-in-detour-target.js +0 -87
  192. package/dist/rules/no-throw-in-detour-target.js.map +0 -1
  193. package/dist/rules/no-throw-in-implementation.d.ts +0 -9
  194. package/dist/rules/no-throw-in-implementation.d.ts.map +0 -1
  195. package/dist/rules/no-throw-in-implementation.js +0 -34
  196. package/dist/rules/no-throw-in-implementation.js.map +0 -1
  197. package/dist/rules/prefer-schema-inference.d.ts +0 -7
  198. package/dist/rules/prefer-schema-inference.d.ts.map +0 -1
  199. package/dist/rules/prefer-schema-inference.js +0 -86
  200. package/dist/rules/prefer-schema-inference.js.map +0 -1
  201. package/dist/rules/provision-declarations.d.ts +0 -14
  202. package/dist/rules/provision-declarations.d.ts.map +0 -1
  203. package/dist/rules/provision-declarations.js +0 -344
  204. package/dist/rules/provision-declarations.js.map +0 -1
  205. package/dist/rules/provision-exists.d.ts +0 -6
  206. package/dist/rules/provision-exists.d.ts.map +0 -1
  207. package/dist/rules/provision-exists.js +0 -90
  208. package/dist/rules/provision-exists.js.map +0 -1
  209. package/dist/rules/scan.d.ts +0 -8
  210. package/dist/rules/scan.d.ts.map +0 -1
  211. package/dist/rules/scan.js +0 -32
  212. package/dist/rules/scan.js.map +0 -1
  213. package/dist/rules/service-declarations.d.ts +0 -16
  214. package/dist/rules/service-declarations.d.ts.map +0 -1
  215. package/dist/rules/service-declarations.js +0 -346
  216. package/dist/rules/service-declarations.js.map +0 -1
  217. package/dist/rules/service-exists.d.ts +0 -8
  218. package/dist/rules/service-exists.d.ts.map +0 -1
  219. package/dist/rules/service-exists.js +0 -91
  220. package/dist/rules/service-exists.js.map +0 -1
  221. package/dist/rules/specs.d.ts +0 -29
  222. package/dist/rules/specs.d.ts.map +0 -1
  223. package/dist/rules/specs.js +0 -192
  224. package/dist/rules/specs.js.map +0 -1
  225. package/dist/rules/structure.d.ts +0 -13
  226. package/dist/rules/structure.d.ts.map +0 -1
  227. package/dist/rules/structure.js +0 -142
  228. package/dist/rules/structure.js.map +0 -1
  229. package/dist/rules/types.d.ts +0 -54
  230. package/dist/rules/types.d.ts.map +0 -1
  231. package/dist/rules/types.js +0 -2
  232. package/dist/rules/types.js.map +0 -1
  233. package/dist/rules/valid-describe-refs.d.ts +0 -7
  234. package/dist/rules/valid-describe-refs.d.ts.map +0 -1
  235. package/dist/rules/valid-describe-refs.js +0 -51
  236. package/dist/rules/valid-describe-refs.js.map +0 -1
  237. package/dist/rules/valid-detour-refs.d.ts +0 -6
  238. package/dist/rules/valid-detour-refs.d.ts.map +0 -1
  239. package/dist/rules/valid-detour-refs.js +0 -117
  240. package/dist/rules/valid-detour-refs.js.map +0 -1
  241. package/dist/trails/context-no-surface-types.trail.d.ts +0 -13
  242. package/dist/trails/context-no-surface-types.trail.d.ts.map +0 -1
  243. package/dist/trails/context-no-surface-types.trail.js +0 -21
  244. package/dist/trails/context-no-surface-types.trail.js.map +0 -1
  245. package/dist/trails/context-no-trailhead-types.trail.d.ts +0 -13
  246. package/dist/trails/context-no-trailhead-types.trail.d.ts.map +0 -1
  247. package/dist/trails/context-no-trailhead-types.trail.js +0 -21
  248. package/dist/trails/context-no-trailhead-types.trail.js.map +0 -1
  249. package/dist/trails/cross-declarations.trail.d.ts +0 -13
  250. package/dist/trails/cross-declarations.trail.d.ts.map +0 -1
  251. package/dist/trails/cross-declarations.trail.js +0 -22
  252. package/dist/trails/cross-declarations.trail.js.map +0 -1
  253. package/dist/trails/follow-declarations.trail.d.ts +0 -13
  254. package/dist/trails/follow-declarations.trail.d.ts.map +0 -1
  255. package/dist/trails/follow-declarations.trail.js +0 -22
  256. package/dist/trails/follow-declarations.trail.js.map +0 -1
  257. package/dist/trails/implementation-returns-result.trail.d.ts +0 -13
  258. package/dist/trails/implementation-returns-result.trail.d.ts.map +0 -1
  259. package/dist/trails/implementation-returns-result.trail.js +0 -20
  260. package/dist/trails/implementation-returns-result.trail.js.map +0 -1
  261. package/dist/trails/index.d.ts +0 -16
  262. package/dist/trails/index.d.ts.map +0 -1
  263. package/dist/trails/index.js +0 -15
  264. package/dist/trails/index.js.map +0 -1
  265. package/dist/trails/no-direct-impl-in-route.trail.d.ts +0 -13
  266. package/dist/trails/no-direct-impl-in-route.trail.d.ts.map +0 -1
  267. package/dist/trails/no-direct-impl-in-route.trail.js +0 -22
  268. package/dist/trails/no-direct-impl-in-route.trail.js.map +0 -1
  269. package/dist/trails/no-direct-implementation-call.trail.d.ts +0 -13
  270. package/dist/trails/no-direct-implementation-call.trail.d.ts.map +0 -1
  271. package/dist/trails/no-direct-implementation-call.trail.js +0 -16
  272. package/dist/trails/no-direct-implementation-call.trail.js.map +0 -1
  273. package/dist/trails/no-sync-result-assumption.trail.d.ts +0 -13
  274. package/dist/trails/no-sync-result-assumption.trail.d.ts.map +0 -1
  275. package/dist/trails/no-sync-result-assumption.trail.js +0 -19
  276. package/dist/trails/no-sync-result-assumption.trail.js.map +0 -1
  277. package/dist/trails/no-throw-in-detour-target.trail.d.ts +0 -15
  278. package/dist/trails/no-throw-in-detour-target.trail.d.ts.map +0 -1
  279. package/dist/trails/no-throw-in-detour-target.trail.js +0 -20
  280. package/dist/trails/no-throw-in-detour-target.trail.js.map +0 -1
  281. package/dist/trails/no-throw-in-implementation.trail.d.ts +0 -13
  282. package/dist/trails/no-throw-in-implementation.trail.d.ts.map +0 -1
  283. package/dist/trails/no-throw-in-implementation.trail.js +0 -20
  284. package/dist/trails/no-throw-in-implementation.trail.js.map +0 -1
  285. package/dist/trails/prefer-schema-inference.trail.d.ts +0 -13
  286. package/dist/trails/prefer-schema-inference.trail.d.ts.map +0 -1
  287. package/dist/trails/prefer-schema-inference.trail.js +0 -21
  288. package/dist/trails/prefer-schema-inference.trail.js.map +0 -1
  289. package/dist/trails/provision-declarations.trail.d.ts +0 -13
  290. package/dist/trails/provision-declarations.trail.d.ts.map +0 -1
  291. package/dist/trails/provision-declarations.trail.js +0 -25
  292. package/dist/trails/provision-declarations.trail.js.map +0 -1
  293. package/dist/trails/provision-exists.trail.d.ts +0 -15
  294. package/dist/trails/provision-exists.trail.d.ts.map +0 -1
  295. package/dist/trails/provision-exists.trail.js +0 -27
  296. package/dist/trails/provision-exists.trail.js.map +0 -1
  297. package/dist/trails/run.d.ts +0 -17
  298. package/dist/trails/run.d.ts.map +0 -1
  299. package/dist/trails/run.js +0 -39
  300. package/dist/trails/run.js.map +0 -1
  301. package/dist/trails/schema.d.ts +0 -53
  302. package/dist/trails/schema.d.ts.map +0 -1
  303. package/dist/trails/schema.js +0 -42
  304. package/dist/trails/schema.js.map +0 -1
  305. package/dist/trails/service-declarations.trail.d.ts +0 -26
  306. package/dist/trails/service-declarations.trail.d.ts.map +0 -1
  307. package/dist/trails/service-declarations.trail.js +0 -27
  308. package/dist/trails/service-declarations.trail.js.map +0 -1
  309. package/dist/trails/service-exists.trail.d.ts +0 -32
  310. package/dist/trails/service-exists.trail.d.ts.map +0 -1
  311. package/dist/trails/service-exists.trail.js +0 -29
  312. package/dist/trails/service-exists.trail.js.map +0 -1
  313. package/dist/trails/topo.d.ts +0 -3
  314. package/dist/trails/topo.d.ts.map +0 -1
  315. package/dist/trails/topo.js +0 -5
  316. package/dist/trails/topo.js.map +0 -1
  317. package/dist/trails/valid-describe-refs.trail.d.ts +0 -15
  318. package/dist/trails/valid-describe-refs.trail.d.ts.map +0 -1
  319. package/dist/trails/valid-describe-refs.trail.js +0 -18
  320. package/dist/trails/valid-describe-refs.trail.js.map +0 -1
  321. package/dist/trails/valid-detour-refs.trail.d.ts +0 -15
  322. package/dist/trails/valid-detour-refs.trail.d.ts.map +0 -1
  323. package/dist/trails/valid-detour-refs.trail.js +0 -24
  324. package/dist/trails/valid-detour-refs.trail.js.map +0 -1
  325. package/dist/trails/wrap-rule.d.ts +0 -29
  326. package/dist/trails/wrap-rule.d.ts.map +0 -1
  327. package/dist/trails/wrap-rule.js +0 -47
  328. package/dist/trails/wrap-rule.js.map +0 -1
  329. package/src/__tests__/cli.test.ts +0 -311
  330. package/src/__tests__/cross-declarations.test.ts +0 -303
  331. package/src/__tests__/drift.test.ts +0 -144
  332. package/src/__tests__/formatters.test.ts +0 -157
  333. package/src/__tests__/implementation-returns-result.test.ts +0 -129
  334. package/src/__tests__/no-direct-implementation-call.test.ts +0 -83
  335. package/src/__tests__/no-sync-result-assumption.test.ts +0 -85
  336. package/src/__tests__/no-throw-in-detour-target.test.ts +0 -78
  337. package/src/__tests__/prefer-schema-inference.test.ts +0 -84
  338. package/src/__tests__/provision-declarations.test.ts +0 -318
  339. package/src/__tests__/provision-exists.test.ts +0 -122
  340. package/src/__tests__/rules.test.ts +0 -227
  341. package/src/__tests__/trails.test.ts +0 -19
  342. package/src/__tests__/valid-describe-refs.test.ts +0 -60
  343. package/src/__tests__/wrap-rule.test.ts +0 -41
  344. package/src/rules/no-direct-impl-in-route.ts +0 -81
  345. package/src/rules/no-throw-in-detour-target.ts +0 -150
  346. package/src/rules/valid-detour-refs.ts +0 -189
  347. package/src/trails/no-direct-impl-in-route.trail.ts +0 -22
  348. package/src/trails/no-throw-in-detour-target.trail.ts +0 -20
  349. package/src/trails/valid-detour-refs.trail.ts +0 -24
  350. package/tsconfig.json +0 -9
  351. package/tsconfig.tsbuildinfo +0 -1
@@ -0,0 +1,150 @@
1
+ import { filterSurfaceTrails, zodToJsonSchema } from '@ontrails/core';
2
+ import type { AnyTrail, Topo } from '@ontrails/core';
3
+
4
+ import type { TopoAwareWardenRule, WardenDiagnostic } from './types.js';
5
+
6
+ const RULE_NAME = 'public-union-output-discriminants';
7
+
8
+ type JsonSchema = Readonly<Record<string, unknown>>;
9
+
10
+ interface ObjectBranch {
11
+ readonly properties: Readonly<Record<string, JsonSchema>>;
12
+ readonly required: ReadonlySet<string>;
13
+ }
14
+
15
+ const isRecord = (value: unknown): value is Record<string, unknown> =>
16
+ Boolean(value && typeof value === 'object' && !Array.isArray(value));
17
+
18
+ const isJsonSchema = (value: unknown): value is JsonSchema => isRecord(value);
19
+
20
+ const schemaForTrailOutput = (trail: AnyTrail): JsonSchema | undefined => {
21
+ if (!trail.output) {
22
+ return undefined;
23
+ }
24
+ try {
25
+ return zodToJsonSchema(trail.output);
26
+ } catch {
27
+ return undefined;
28
+ }
29
+ };
30
+
31
+ const readProperties = (
32
+ schema: JsonSchema
33
+ ): Readonly<Record<string, JsonSchema>> | undefined => {
34
+ const { properties } = schema;
35
+ if (!isRecord(properties)) {
36
+ return undefined;
37
+ }
38
+ const entries = Object.entries(properties);
39
+ if (!entries.every(([, value]) => isJsonSchema(value))) {
40
+ return undefined;
41
+ }
42
+ return properties as Readonly<Record<string, JsonSchema>>;
43
+ };
44
+
45
+ const readRequired = (schema: JsonSchema): ReadonlySet<string> => {
46
+ const { required } = schema;
47
+ if (!Array.isArray(required)) {
48
+ return new Set();
49
+ }
50
+ return new Set(
51
+ required.filter((entry): entry is string => typeof entry === 'string')
52
+ );
53
+ };
54
+
55
+ const objectBranchFromSchema = (schema: unknown): ObjectBranch | undefined => {
56
+ if (!isJsonSchema(schema) || schema['type'] !== 'object') {
57
+ return undefined;
58
+ }
59
+ const properties = readProperties(schema);
60
+ if (!properties) {
61
+ return undefined;
62
+ }
63
+ return { properties, required: readRequired(schema) };
64
+ };
65
+
66
+ const objectBranchesFromAnyOf = (
67
+ schema: JsonSchema
68
+ ): readonly ObjectBranch[] | undefined => {
69
+ const { anyOf } = schema;
70
+ if (!Array.isArray(anyOf) || anyOf.length < 2) {
71
+ return undefined;
72
+ }
73
+ const branches = anyOf.flatMap((branch) => {
74
+ const objectBranch = objectBranchFromSchema(branch);
75
+ return objectBranch ? [objectBranch] : [];
76
+ });
77
+ return branches.length >= 2 ? branches : undefined;
78
+ };
79
+
80
+ const hasConst = (schema: JsonSchema): boolean =>
81
+ Object.hasOwn(schema, 'const');
82
+
83
+ const constValue = (schema: JsonSchema): unknown => schema['const'];
84
+
85
+ const constKey = (value: unknown): string => JSON.stringify(value);
86
+
87
+ const branchLiteralForKey = (
88
+ branch: ObjectBranch,
89
+ key: string
90
+ ): unknown | undefined => {
91
+ if (!branch.required.has(key)) {
92
+ return undefined;
93
+ }
94
+ const property = branch.properties[key];
95
+ return property && hasConst(property) ? constValue(property) : undefined;
96
+ };
97
+
98
+ const hasRequiredLiteralDiscriminant = (
99
+ branches: readonly ObjectBranch[]
100
+ ): boolean => {
101
+ const [first] = branches;
102
+ if (!first) {
103
+ return false;
104
+ }
105
+
106
+ return Object.keys(first.properties).some((key) => {
107
+ const values = branches.map((branch) => branchLiteralForKey(branch, key));
108
+ return (
109
+ values.every((value) => value !== undefined) &&
110
+ new Set(values.map(constKey)).size === branches.length
111
+ );
112
+ });
113
+ };
114
+
115
+ const diagnosticForTrail = (trail: AnyTrail): WardenDiagnostic => ({
116
+ filePath: '<topo>',
117
+ line: 1,
118
+ message:
119
+ `Trail "${trail.id}" exposes a public output anyOf with object variants but no required literal discriminator. ` +
120
+ 'Add a shared z.literal(...) field or z.discriminatedUnion(...) so surfaces and agents can select the output branch.',
121
+ rule: RULE_NAME,
122
+ severity: 'error',
123
+ });
124
+
125
+ const diagnoseTrail = (trail: AnyTrail): WardenDiagnostic | undefined => {
126
+ const schema = schemaForTrailOutput(trail);
127
+ if (!schema) {
128
+ return undefined;
129
+ }
130
+ const objectBranches = objectBranchesFromAnyOf(schema);
131
+ if (!objectBranches) {
132
+ return undefined;
133
+ }
134
+ return hasRequiredLiteralDiscriminant(objectBranches)
135
+ ? undefined
136
+ : diagnosticForTrail(trail);
137
+ };
138
+
139
+ export const publicUnionOutputDiscriminants: TopoAwareWardenRule = {
140
+ checkTopo(topo: Topo): readonly WardenDiagnostic[] {
141
+ return filterSurfaceTrails(topo.list()).flatMap((trail) => {
142
+ const diagnostic = diagnoseTrail(trail);
143
+ return diagnostic ? [diagnostic] : [];
144
+ });
145
+ },
146
+ description:
147
+ 'Require public trail output object unions to expose a required literal discriminator.',
148
+ name: RULE_NAME,
149
+ severity: 'error',
150
+ };
@@ -0,0 +1,187 @@
1
+ import {
2
+ buildSignalIdentifierResolver,
3
+ deriveConstString,
4
+ extractStringLiteral,
5
+ findConfigProperty,
6
+ findTrailDefinitions,
7
+ getStringValue,
8
+ identifierName,
9
+ isStringLiteral,
10
+ offsetToLine,
11
+ parse,
12
+ walk,
13
+ } from './ast.js';
14
+ import type { AstNode, SignalIdentifierResolver } from './ast.js';
15
+ import { isTestFile } from './scan.js';
16
+ import type { WardenDiagnostic, WardenRule } from './types.js';
17
+
18
+ interface DeclaredFireSummary {
19
+ readonly count: number;
20
+ readonly ids: readonly string[];
21
+ readonly line: number;
22
+ }
23
+
24
+ const isReadIntent = (config: AstNode): boolean => {
25
+ const intentProp = findConfigProperty(config, 'intent');
26
+ const intentValue = intentProp?.value as AstNode | undefined;
27
+ return isStringLiteral(intentValue) && getStringValue(intentValue) === 'read';
28
+ };
29
+
30
+ const collectArrayBindings = (ast: AstNode): ReadonlyMap<string, AstNode> => {
31
+ const bindings = new Map<string, AstNode>();
32
+ walk(ast, (node) => {
33
+ if (node.type !== 'VariableDeclarator') {
34
+ return;
35
+ }
36
+
37
+ const { id, init } = node as unknown as {
38
+ readonly id?: AstNode;
39
+ readonly init?: AstNode;
40
+ };
41
+ const name = identifierName(id);
42
+ if (name && init?.type === 'ArrayExpression') {
43
+ bindings.set(name, init);
44
+ }
45
+ });
46
+ return bindings;
47
+ };
48
+
49
+ const getFiresArray = (
50
+ config: AstNode,
51
+ arrayBindings: ReadonlyMap<string, AstNode>
52
+ ): AstNode | null => {
53
+ const firesProp = findConfigProperty(config, 'fires');
54
+ const value = firesProp?.value as AstNode | undefined;
55
+ if (value?.type === 'ArrayExpression') {
56
+ return value;
57
+ }
58
+
59
+ const name = identifierName(value);
60
+ return name ? (arrayBindings.get(name) ?? null) : null;
61
+ };
62
+
63
+ const getFiresElements = (
64
+ config: AstNode,
65
+ arrayBindings: ReadonlyMap<string, AstNode>
66
+ ): readonly AstNode[] => {
67
+ const array = getFiresArray(config, arrayBindings);
68
+ if (!array) {
69
+ return [];
70
+ }
71
+
72
+ return (
73
+ (array as unknown as { readonly elements?: readonly (AstNode | null)[] })
74
+ .elements ?? []
75
+ ).filter((element): element is AstNode => element !== null);
76
+ };
77
+
78
+ const resolveFireElementId = (
79
+ element: AstNode,
80
+ sourceCode: string,
81
+ signalIds: SignalIdentifierResolver
82
+ ): string | null => {
83
+ const literalValue = extractStringLiteral(element);
84
+ if (literalValue !== null) {
85
+ return literalValue;
86
+ }
87
+
88
+ if (element.type !== 'Identifier') {
89
+ return null;
90
+ }
91
+
92
+ const resolved = signalIds.resolve(element);
93
+ if (resolved.kind === 'signal' || resolved.kind === 'string') {
94
+ return resolved.id;
95
+ }
96
+
97
+ const name = identifierName(element);
98
+ return name ? deriveConstString(name, sourceCode) : null;
99
+ };
100
+
101
+ const summarizeDeclaredFires = (
102
+ config: AstNode,
103
+ arrayBindings: ReadonlyMap<string, AstNode>,
104
+ sourceCode: string,
105
+ signalIds: SignalIdentifierResolver
106
+ ): DeclaredFireSummary | null => {
107
+ const firesProp = findConfigProperty(config, 'fires');
108
+ const elements = getFiresElements(config, arrayBindings);
109
+ if (elements.length === 0) {
110
+ return null;
111
+ }
112
+
113
+ const ids: string[] = [];
114
+ for (const element of elements) {
115
+ const resolved = resolveFireElementId(element, sourceCode, signalIds);
116
+ if (resolved) {
117
+ ids.push(resolved);
118
+ }
119
+ }
120
+
121
+ const [firstElement] = elements;
122
+ const lineNode = firesProp ?? firstElement;
123
+ return {
124
+ count: elements.length,
125
+ ids,
126
+ line: lineNode ? offsetToLine(sourceCode, lineNode.start) : 1,
127
+ };
128
+ };
129
+
130
+ const formatSignalList = (summary: DeclaredFireSummary): string => {
131
+ const named = summary.ids.map((id) => `"${id}"`);
132
+ const unresolvedCount = summary.count - summary.ids.length;
133
+ const unresolved =
134
+ unresolvedCount > 0
135
+ ? [
136
+ unresolvedCount === 1
137
+ ? '1 unresolved signal reference'
138
+ : `${unresolvedCount} unresolved signal references`,
139
+ ]
140
+ : [];
141
+ return [...named, ...unresolved].join(', ');
142
+ };
143
+
144
+ const buildDiagnostic = (
145
+ trailId: string,
146
+ summary: DeclaredFireSummary,
147
+ filePath: string
148
+ ): WardenDiagnostic => ({
149
+ filePath,
150
+ line: summary.line,
151
+ message: `Trail "${trailId}" declares intent: 'read' but also declares fires: [${formatSignalList(summary)}]. Read trails should remain side-effect-free; change the trail intent or move ctx.fire behavior to an appropriate write trail.`,
152
+ rule: 'read-intent-fires',
153
+ severity: 'warn',
154
+ });
155
+
156
+ export const readIntentFires: WardenRule = {
157
+ check(sourceCode: string, filePath: string): readonly WardenDiagnostic[] {
158
+ if (isTestFile(filePath)) {
159
+ return [];
160
+ }
161
+
162
+ const ast = parse(filePath, sourceCode);
163
+ if (!ast) {
164
+ return [];
165
+ }
166
+
167
+ const signalIds = buildSignalIdentifierResolver(ast);
168
+ const arrayBindings = collectArrayBindings(ast);
169
+ return findTrailDefinitions(ast).flatMap((def) => {
170
+ if (def.kind !== 'trail' || !isReadIntent(def.config)) {
171
+ return [];
172
+ }
173
+
174
+ const summary = summarizeDeclaredFires(
175
+ def.config,
176
+ arrayBindings,
177
+ sourceCode,
178
+ signalIds
179
+ );
180
+ return summary ? [buildDiagnostic(def.id, summary, filePath)] : [];
181
+ });
182
+ },
183
+ description:
184
+ 'Warn when read-intent trails declare signal fires side effects.',
185
+ name: 'read-intent-fires',
186
+ severity: 'warn',
187
+ };
@@ -0,0 +1,98 @@
1
+ import {
2
+ collectContourDefinitionIds,
3
+ collectContourReferenceSites,
4
+ offsetToLine,
5
+ parse,
6
+ } from './ast.js';
7
+ import type { AstNode } from './ast.js';
8
+ import { mergeKnownContourIds } from './contour-ids.js';
9
+ import { isTestFile } from './scan.js';
10
+ import type {
11
+ ProjectAwareWardenRule,
12
+ ProjectContext,
13
+ WardenDiagnostic,
14
+ } from './types.js';
15
+
16
+ const buildMissingReferenceDiagnostic = (
17
+ sourceContour: string,
18
+ field: string,
19
+ targetContour: string,
20
+ filePath: string,
21
+ line: number
22
+ ): WardenDiagnostic => ({
23
+ filePath,
24
+ line,
25
+ message: `Contour "${sourceContour}" field "${field}" references contour "${targetContour}" which is not defined in the project.`,
26
+ rule: 'reference-exists',
27
+ severity: 'error',
28
+ });
29
+
30
+ const checkContourReferences = (
31
+ ast: AstNode,
32
+ sourceCode: string,
33
+ filePath: string,
34
+ knownContourIds: ReadonlySet<string>
35
+ ): readonly WardenDiagnostic[] => {
36
+ if (isTestFile(filePath)) {
37
+ return [];
38
+ }
39
+
40
+ return collectContourReferenceSites(ast, knownContourIds).flatMap(
41
+ (reference) => {
42
+ if (knownContourIds.has(reference.target)) {
43
+ return [];
44
+ }
45
+
46
+ return [
47
+ buildMissingReferenceDiagnostic(
48
+ reference.source,
49
+ reference.field,
50
+ reference.target,
51
+ filePath,
52
+ offsetToLine(sourceCode, reference.start)
53
+ ),
54
+ ];
55
+ }
56
+ );
57
+ };
58
+
59
+ /**
60
+ * Checks that every contour `.id()` reference resolves to a known contour.
61
+ */
62
+ export const referenceExists: ProjectAwareWardenRule = {
63
+ check(sourceCode: string, filePath: string): readonly WardenDiagnostic[] {
64
+ const ast = parse(filePath, sourceCode);
65
+ if (!ast) {
66
+ return [];
67
+ }
68
+
69
+ return checkContourReferences(
70
+ ast,
71
+ sourceCode,
72
+ filePath,
73
+ collectContourDefinitionIds(ast)
74
+ );
75
+ },
76
+ checkWithContext(
77
+ sourceCode: string,
78
+ filePath: string,
79
+ context: ProjectContext
80
+ ): readonly WardenDiagnostic[] {
81
+ const ast = parse(filePath, sourceCode);
82
+ if (!ast) {
83
+ return [];
84
+ }
85
+
86
+ const localContourIds = collectContourDefinitionIds(ast);
87
+ return checkContourReferences(
88
+ ast,
89
+ sourceCode,
90
+ filePath,
91
+ mergeKnownContourIds(localContourIds, context.knownContourIds)
92
+ );
93
+ },
94
+ description:
95
+ 'Ensure every contour field declared via .id() resolves to a known contour.',
96
+ name: 'reference-exists',
97
+ severity: 'error',
98
+ };
@@ -0,0 +1,113 @@
1
+ /**
2
+ * Registry name snapshot used by `warden-export-symmetry`.
3
+ *
4
+ * Imports each rule module directly to avoid a dependency cycle with
5
+ * `./index.ts`, which itself imports `warden-export-symmetry`. Keep this
6
+ * list in lockstep with `wardenRules` / `wardenTopoRules` in `./index.ts` —
7
+ * the `warden-export-symmetry` rule will fail the build if they drift.
8
+ */
9
+ import { activationOrphan } from './activation-orphan.js';
10
+ import { circularRefs } from './circular-refs.js';
11
+ import { contourExists } from './contour-exists.js';
12
+ import { contextNoSurfaceTypes } from './context-no-surface-types.js';
13
+ import { crossDeclarations } from './cross-declarations.js';
14
+ import { deadInternalTrail } from './dead-internal-trail.js';
15
+ import { draftFileMarking } from './draft-file-marking.js';
16
+ import { draftVisibleDebt } from './draft-visible-debt.js';
17
+ import { errorMappingCompleteness } from './error-mapping-completeness.js';
18
+ import { exampleValid } from './example-valid.js';
19
+ import { firesDeclarations } from './fires-declarations.js';
20
+ import { implementationReturnsResult } from './implementation-returns-result.js';
21
+ import { incompleteAccessorForStandardOp } from './incomplete-accessor-for-standard-op.js';
22
+ import { incompleteCrud } from './incomplete-crud.js';
23
+ import { intentPropagation } from './intent-propagation.js';
24
+ import { layerFieldNameDrift } from './layer-field-name-drift.js';
25
+ import { missingReconcile } from './missing-reconcile.js';
26
+ import { missingVisibility } from './missing-visibility.js';
27
+ import { noDevPermitInSource } from './no-dev-permit-in-source.js';
28
+ import { noLegacyLayerImports } from './no-legacy-layer-imports.js';
29
+ import { noDirectImplementationCall } from './no-direct-implementation-call.js';
30
+ import { noNativeErrorResult } from './no-native-error-result.js';
31
+ import { noSyncResultAssumption } from './no-sync-result-assumption.js';
32
+ import { noThrowInDetourRecover } from './no-throw-in-detour-recover.js';
33
+ import { noThrowInImplementation } from './no-throw-in-implementation.js';
34
+ import { onReferencesExist } from './on-references-exist.js';
35
+ import { orphanedSignal } from './orphaned-signal.js';
36
+ import { ownerProjectionParity } from './owner-projection-parity.js';
37
+ import { permitGovernance } from './permit-governance.js';
38
+ import { preferSchemaInference } from './prefer-schema-inference.js';
39
+ import { publicInternalDeepImports } from './public-internal-deep-imports.js';
40
+ import { publicOutputSchema } from './public-output-schema.js';
41
+ import { publicUnionOutputDiscriminants } from './public-union-output-discriminants.js';
42
+ import { readIntentFires } from './read-intent-fires.js';
43
+ import { referenceExists } from './reference-exists.js';
44
+ import { resolvedImportBoundary } from './resolved-import-boundary.js';
45
+ import { resourceDeclarations } from './resource-declarations.js';
46
+ import { resourceExists } from './resource-exists.js';
47
+ import { resourceIdGrammar } from './resource-id-grammar.js';
48
+ import { scheduledDestroyIntent } from './scheduled-destroy-intent.js';
49
+ import { signalGraphCoaching } from './signal-graph-coaching.js';
50
+ import { staticResourceAccessorPreference } from './static-resource-accessor-preference.js';
51
+ import { unmaterializedActivationSource } from './unmaterialized-activation-source.js';
52
+ import { unreachableDetourShadowing } from './unreachable-detour-shadowing.js';
53
+ import { validDetourContract } from './valid-detour-contract.js';
54
+ import { validDescribeRefs } from './valid-describe-refs.js';
55
+ import { wardenRulesUseAst } from './warden-rules-use-ast.js';
56
+ import { webhookRouteCollision } from './webhook-route-collision.js';
57
+
58
+ /**
59
+ * All non-`warden-export-symmetry` rule identifiers registered in
60
+ * `wardenRules` / `wardenTopoRules`. Excludes the symmetry rule itself to
61
+ * avoid a self-referential check; the symmetry rule adds its own name back in
62
+ * when comparing against the public barrel.
63
+ */
64
+ export const registeredRuleNames: readonly string[] = [
65
+ activationOrphan.name,
66
+ circularRefs.name,
67
+ contextNoSurfaceTypes.name,
68
+ contourExists.name,
69
+ crossDeclarations.name,
70
+ deadInternalTrail.name,
71
+ draftFileMarking.name,
72
+ draftVisibleDebt.name,
73
+ errorMappingCompleteness.name,
74
+ exampleValid.name,
75
+ firesDeclarations.name,
76
+ implementationReturnsResult.name,
77
+ incompleteAccessorForStandardOp.name,
78
+ incompleteCrud.name,
79
+ intentPropagation.name,
80
+ layerFieldNameDrift.name,
81
+ missingReconcile.name,
82
+ missingVisibility.name,
83
+ noDevPermitInSource.name,
84
+ noLegacyLayerImports.name,
85
+ noDirectImplementationCall.name,
86
+ noNativeErrorResult.name,
87
+ noSyncResultAssumption.name,
88
+ noThrowInDetourRecover.name,
89
+ noThrowInImplementation.name,
90
+ onReferencesExist.name,
91
+ orphanedSignal.name,
92
+ ownerProjectionParity.name,
93
+ permitGovernance.name,
94
+ preferSchemaInference.name,
95
+ publicInternalDeepImports.name,
96
+ publicOutputSchema.name,
97
+ publicUnionOutputDiscriminants.name,
98
+ readIntentFires.name,
99
+ referenceExists.name,
100
+ resolvedImportBoundary.name,
101
+ resourceDeclarations.name,
102
+ resourceExists.name,
103
+ resourceIdGrammar.name,
104
+ scheduledDestroyIntent.name,
105
+ signalGraphCoaching.name,
106
+ staticResourceAccessorPreference.name,
107
+ unmaterializedActivationSource.name,
108
+ unreachableDetourShadowing.name,
109
+ validDetourContract.name,
110
+ validDescribeRefs.name,
111
+ wardenRulesUseAst.name,
112
+ webhookRouteCollision.name,
113
+ ];
@@ -0,0 +1,146 @@
1
+ import { hasIgnoreCommentOnLine, splitSourceLines } from './ast.js';
2
+ import { isTestFile } from './scan.js';
3
+ import type { WardenImportResolution } from '../resolve.js';
4
+ import type {
5
+ ProjectAwareWardenRule,
6
+ ProjectContext,
7
+ WardenDiagnostic,
8
+ } from './types.js';
9
+
10
+ const RULE_NAME = 'resolved-import-boundary';
11
+
12
+ const normalizePath = (path: string): string => path.replaceAll('\\', '/');
13
+
14
+ const isLocalPathImport = (importSource: string): boolean =>
15
+ importSource.startsWith('.') || importSource.startsWith('/');
16
+
17
+ const isFixtureOrMigrationFile = (filePath: string): boolean => {
18
+ const normalized = normalizePath(filePath);
19
+ return /(?:^|\/)(?:__fixtures__|fixtures?|migrations?)(?:\/|$)/.test(
20
+ normalized
21
+ );
22
+ };
23
+
24
+ const isAllowlistedFile = (filePath: string): boolean =>
25
+ isTestFile(filePath) || isFixtureOrMigrationFile(filePath);
26
+
27
+ const resolutionLabel = (resolution: {
28
+ readonly importSource: string;
29
+ readonly packageName?: string | undefined;
30
+ }): string => resolution.packageName ?? resolution.importSource;
31
+
32
+ const publicSurfaceMessage = (resolution: {
33
+ readonly importSource: string;
34
+ readonly packageName?: string | undefined;
35
+ }): string =>
36
+ `Import "${resolution.importSource}" is not exported by ${resolutionLabel(
37
+ resolution
38
+ )}. Import the package root or an exported subpath instead.`;
39
+
40
+ const localPathBoundaryMessage = (resolution: {
41
+ readonly importSource: string;
42
+ readonly packageName?: string | undefined;
43
+ }): string =>
44
+ `Local import "${resolution.importSource}" crosses into ${resolutionLabel(
45
+ resolution
46
+ )}. Import the target package public surface instead.`;
47
+
48
+ const internalTargetMessage = (resolution: {
49
+ readonly importSource: string;
50
+ readonly packageName?: string | undefined;
51
+ }): string =>
52
+ `Import "${resolution.importSource}" targets internal/private files in ${resolutionLabel(
53
+ resolution
54
+ )}. Import the target package public surface instead.`;
55
+
56
+ const diagnosticForResolution = (
57
+ filePath: string,
58
+ resolution: WardenImportResolution
59
+ ): WardenDiagnostic | null => {
60
+ if (!resolution.crossesPackageBoundary) {
61
+ return null;
62
+ }
63
+
64
+ if (resolution.isInternalTarget) {
65
+ return {
66
+ filePath,
67
+ line: resolution.line,
68
+ message: internalTargetMessage(resolution),
69
+ rule: RULE_NAME,
70
+ severity: 'error',
71
+ };
72
+ }
73
+
74
+ if (isLocalPathImport(resolution.importSource)) {
75
+ return {
76
+ filePath,
77
+ line: resolution.line,
78
+ message: localPathBoundaryMessage(resolution),
79
+ rule: RULE_NAME,
80
+ severity: 'error',
81
+ };
82
+ }
83
+
84
+ if (resolution.errorKind === 'package-path-not-exported') {
85
+ return {
86
+ filePath,
87
+ line: resolution.line,
88
+ message: publicSurfaceMessage(resolution),
89
+ rule: RULE_NAME,
90
+ severity: 'error',
91
+ };
92
+ }
93
+
94
+ if (
95
+ resolution.errorKind &&
96
+ resolution.errorKind !== 'builtin' &&
97
+ resolution.errorKind !== 'ignored'
98
+ ) {
99
+ return {
100
+ filePath,
101
+ line: resolution.line,
102
+ message: publicSurfaceMessage(resolution),
103
+ rule: RULE_NAME,
104
+ severity: 'error',
105
+ };
106
+ }
107
+
108
+ return null;
109
+ };
110
+
111
+ const importResolutionsForFile = (context: ProjectContext, filePath: string) =>
112
+ context.importResolutionsByFile?.get(filePath) ?? [];
113
+
114
+ export const resolvedImportBoundary: ProjectAwareWardenRule = {
115
+ check(): readonly WardenDiagnostic[] {
116
+ return [];
117
+ },
118
+ checkWithContext(
119
+ sourceCode: string,
120
+ filePath: string,
121
+ context: ProjectContext
122
+ ): readonly WardenDiagnostic[] {
123
+ if (isAllowlistedFile(filePath)) {
124
+ return [];
125
+ }
126
+
127
+ const lines = splitSourceLines(sourceCode);
128
+ const diagnostics: WardenDiagnostic[] = [];
129
+
130
+ for (const resolution of importResolutionsForFile(context, filePath)) {
131
+ if (hasIgnoreCommentOnLine(lines, resolution.line)) {
132
+ continue;
133
+ }
134
+ const diagnostic = diagnosticForResolution(filePath, resolution);
135
+ if (diagnostic) {
136
+ diagnostics.push(diagnostic);
137
+ }
138
+ }
139
+
140
+ return diagnostics;
141
+ },
142
+ description:
143
+ 'Ensure cross-package imports resolve through package-owned public exports.',
144
+ name: RULE_NAME,
145
+ severity: 'error',
146
+ };