@ontrails/warden 1.0.0-beta.15 → 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 (486) hide show
  1. package/CHANGELOG.md +132 -1
  2. package/README.md +64 -30
  3. package/bin/warden.ts +22 -0
  4. package/package.json +25 -8
  5. package/src/ast.ts +28 -0
  6. package/src/cli.ts +740 -46
  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 +109 -14
  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 +233 -31
  17. package/src/rules/draft-visible-debt.ts +2 -2
  18. package/src/rules/error-mapping-completeness.ts +24 -9
  19. package/src/rules/fires-declarations.ts +201 -52
  20. package/src/rules/incomplete-accessor-for-standard-op.ts +8 -51
  21. package/src/rules/incomplete-crud.ts +8 -7
  22. package/src/rules/index.ts +67 -3
  23. package/src/rules/intent-propagation.ts +6 -21
  24. package/src/rules/layer-field-name-drift.ts +96 -0
  25. package/src/rules/metadata.ts +508 -0
  26. package/src/rules/missing-visibility.ts +1 -1
  27. package/src/rules/no-dev-permit-in-source.ts +99 -0
  28. package/src/rules/no-legacy-layer-imports.ts +193 -0
  29. package/src/rules/no-native-error-result.ts +111 -0
  30. package/src/rules/no-sync-result-assumption.ts +5 -8
  31. package/src/rules/orphaned-signal.ts +1 -1
  32. package/src/rules/owner-projection-parity.ts +146 -0
  33. package/src/rules/public-internal-deep-imports.ts +517 -0
  34. package/src/rules/public-output-schema.ts +29 -0
  35. package/src/rules/public-union-output-discriminants.ts +150 -0
  36. package/src/rules/read-intent-fires.ts +187 -0
  37. package/src/rules/registry-names.ts +32 -2
  38. package/src/rules/resolved-import-boundary.ts +146 -0
  39. package/src/rules/scan.ts +0 -26
  40. package/src/rules/scheduled-destroy-intent.ts +44 -0
  41. package/src/rules/signal-graph-coaching.ts +191 -0
  42. package/src/rules/static-resource-accessor-preference.ts +657 -0
  43. package/src/rules/types.ts +132 -5
  44. package/src/rules/unmaterialized-activation-source.ts +84 -0
  45. package/src/rules/unreachable-detour-shadowing.ts +2 -33
  46. package/src/rules/webhook-route-collision.ts +243 -0
  47. package/src/trails/activation-orphan.trail.ts +81 -0
  48. package/src/trails/error-mapping-completeness.trail.ts +4 -4
  49. package/src/trails/fires-declarations.trail.ts +4 -3
  50. package/src/trails/index.ts +16 -1
  51. package/src/trails/layer-field-name-drift.trail.ts +39 -0
  52. package/src/trails/no-dev-permit-in-source.trail.ts +16 -0
  53. package/src/trails/no-legacy-layer-imports.trail.ts +35 -0
  54. package/src/trails/no-native-error-result.trail.ts +18 -0
  55. package/src/trails/orphaned-signal.trail.ts +1 -1
  56. package/src/trails/owner-projection-parity.trail.ts +26 -0
  57. package/src/trails/public-internal-deep-imports.trail.ts +94 -0
  58. package/src/trails/public-output-schema.trail.ts +55 -0
  59. package/src/trails/public-union-output-discriminants.trail.ts +33 -0
  60. package/src/trails/read-intent-fires.trail.ts +20 -0
  61. package/src/trails/resolved-import-boundary.trail.ts +109 -0
  62. package/src/trails/run.ts +14 -2
  63. package/src/trails/scheduled-destroy-intent.trail.ts +56 -0
  64. package/src/trails/schema.ts +57 -1
  65. package/src/trails/signal-graph-coaching.trail.ts +74 -0
  66. package/src/trails/static-resource-accessor-preference.trail.ts +25 -0
  67. package/src/trails/unmaterialized-activation-source.trail.ts +69 -0
  68. package/src/trails/webhook-route-collision.trail.ts +50 -0
  69. package/src/trails/wrap-rule.ts +30 -3
  70. package/src/workspaces.ts +238 -0
  71. package/.turbo/turbo-build.log +0 -1
  72. package/.turbo/turbo-lint.log +0 -3
  73. package/.turbo/turbo-typecheck.log +0 -1
  74. package/dist/cli.d.ts +0 -63
  75. package/dist/cli.d.ts.map +0 -1
  76. package/dist/cli.js +0 -436
  77. package/dist/cli.js.map +0 -1
  78. package/dist/draft.d.ts +0 -5
  79. package/dist/draft.d.ts.map +0 -1
  80. package/dist/draft.js +0 -16
  81. package/dist/draft.js.map +0 -1
  82. package/dist/drift.d.ts +0 -29
  83. package/dist/drift.d.ts.map +0 -1
  84. package/dist/drift.js +0 -61
  85. package/dist/drift.js.map +0 -1
  86. package/dist/formatters.d.ts +0 -30
  87. package/dist/formatters.d.ts.map +0 -1
  88. package/dist/formatters.js +0 -98
  89. package/dist/formatters.js.map +0 -1
  90. package/dist/index.d.ts +0 -24
  91. package/dist/index.d.ts.map +0 -1
  92. package/dist/index.js +0 -25
  93. package/dist/index.js.map +0 -1
  94. package/dist/rules/ast.d.ts +0 -480
  95. package/dist/rules/ast.d.ts.map +0 -1
  96. package/dist/rules/ast.js +0 -2086
  97. package/dist/rules/ast.js.map +0 -1
  98. package/dist/rules/circular-refs.d.ts +0 -6
  99. package/dist/rules/circular-refs.d.ts.map +0 -1
  100. package/dist/rules/circular-refs.js +0 -83
  101. package/dist/rules/circular-refs.js.map +0 -1
  102. package/dist/rules/context-no-surface-types.d.ts +0 -12
  103. package/dist/rules/context-no-surface-types.d.ts.map +0 -1
  104. package/dist/rules/context-no-surface-types.js +0 -152
  105. package/dist/rules/context-no-surface-types.js.map +0 -1
  106. package/dist/rules/context-no-trailhead-types.d.ts +0 -12
  107. package/dist/rules/context-no-trailhead-types.d.ts.map +0 -1
  108. package/dist/rules/context-no-trailhead-types.js +0 -96
  109. package/dist/rules/context-no-trailhead-types.js.map +0 -1
  110. package/dist/rules/contour-exists.d.ts +0 -7
  111. package/dist/rules/contour-exists.d.ts.map +0 -1
  112. package/dist/rules/contour-exists.js +0 -113
  113. package/dist/rules/contour-exists.js.map +0 -1
  114. package/dist/rules/contour-ids.d.ts +0 -10
  115. package/dist/rules/contour-ids.d.ts.map +0 -1
  116. package/dist/rules/contour-ids.js +0 -12
  117. package/dist/rules/contour-ids.js.map +0 -1
  118. package/dist/rules/cross-declarations.d.ts +0 -13
  119. package/dist/rules/cross-declarations.d.ts.map +0 -1
  120. package/dist/rules/cross-declarations.js +0 -378
  121. package/dist/rules/cross-declarations.js.map +0 -1
  122. package/dist/rules/dead-internal-trail.d.ts +0 -3
  123. package/dist/rules/dead-internal-trail.d.ts.map +0 -1
  124. package/dist/rules/dead-internal-trail.js +0 -80
  125. package/dist/rules/dead-internal-trail.js.map +0 -1
  126. package/dist/rules/draft-file-marking.d.ts +0 -6
  127. package/dist/rules/draft-file-marking.d.ts.map +0 -1
  128. package/dist/rules/draft-file-marking.js +0 -87
  129. package/dist/rules/draft-file-marking.js.map +0 -1
  130. package/dist/rules/draft-visible-debt.d.ts +0 -12
  131. package/dist/rules/draft-visible-debt.d.ts.map +0 -1
  132. package/dist/rules/draft-visible-debt.js +0 -50
  133. package/dist/rules/draft-visible-debt.js.map +0 -1
  134. package/dist/rules/error-mapping-completeness.d.ts +0 -13
  135. package/dist/rules/error-mapping-completeness.d.ts.map +0 -1
  136. package/dist/rules/error-mapping-completeness.js +0 -160
  137. package/dist/rules/error-mapping-completeness.js.map +0 -1
  138. package/dist/rules/example-valid.d.ts +0 -6
  139. package/dist/rules/example-valid.d.ts.map +0 -1
  140. package/dist/rules/example-valid.js +0 -203
  141. package/dist/rules/example-valid.js.map +0 -1
  142. package/dist/rules/fires-declarations.d.ts +0 -16
  143. package/dist/rules/fires-declarations.d.ts.map +0 -1
  144. package/dist/rules/fires-declarations.js +0 -444
  145. package/dist/rules/fires-declarations.js.map +0 -1
  146. package/dist/rules/implementation-returns-result.d.ts +0 -22
  147. package/dist/rules/implementation-returns-result.d.ts.map +0 -1
  148. package/dist/rules/implementation-returns-result.js +0 -839
  149. package/dist/rules/implementation-returns-result.js.map +0 -1
  150. package/dist/rules/incomplete-accessor-for-standard-op.d.ts +0 -30
  151. package/dist/rules/incomplete-accessor-for-standard-op.d.ts.map +0 -1
  152. package/dist/rules/incomplete-accessor-for-standard-op.js +0 -226
  153. package/dist/rules/incomplete-accessor-for-standard-op.js.map +0 -1
  154. package/dist/rules/incomplete-crud.d.ts +0 -21
  155. package/dist/rules/incomplete-crud.d.ts.map +0 -1
  156. package/dist/rules/incomplete-crud.js +0 -368
  157. package/dist/rules/incomplete-crud.js.map +0 -1
  158. package/dist/rules/index.d.ts +0 -51
  159. package/dist/rules/index.d.ts.map +0 -1
  160. package/dist/rules/index.js +0 -119
  161. package/dist/rules/index.js.map +0 -1
  162. package/dist/rules/intent-propagation.d.ts +0 -3
  163. package/dist/rules/intent-propagation.d.ts.map +0 -1
  164. package/dist/rules/intent-propagation.js +0 -57
  165. package/dist/rules/intent-propagation.js.map +0 -1
  166. package/dist/rules/missing-reconcile.d.ts +0 -3
  167. package/dist/rules/missing-reconcile.d.ts.map +0 -1
  168. package/dist/rules/missing-reconcile.js +0 -44
  169. package/dist/rules/missing-reconcile.js.map +0 -1
  170. package/dist/rules/missing-visibility.d.ts +0 -3
  171. package/dist/rules/missing-visibility.d.ts.map +0 -1
  172. package/dist/rules/missing-visibility.js +0 -63
  173. package/dist/rules/missing-visibility.js.map +0 -1
  174. package/dist/rules/no-direct-impl-in-route.d.ts +0 -12
  175. package/dist/rules/no-direct-impl-in-route.d.ts.map +0 -1
  176. package/dist/rules/no-direct-impl-in-route.js +0 -44
  177. package/dist/rules/no-direct-impl-in-route.js.map +0 -1
  178. package/dist/rules/no-direct-implementation-call.d.ts +0 -12
  179. package/dist/rules/no-direct-implementation-call.d.ts.map +0 -1
  180. package/dist/rules/no-direct-implementation-call.js +0 -39
  181. package/dist/rules/no-direct-implementation-call.js.map +0 -1
  182. package/dist/rules/no-sync-result-assumption.d.ts +0 -6
  183. package/dist/rules/no-sync-result-assumption.d.ts.map +0 -1
  184. package/dist/rules/no-sync-result-assumption.js +0 -907
  185. package/dist/rules/no-sync-result-assumption.js.map +0 -1
  186. package/dist/rules/no-throw-in-detour-recover.d.ts +0 -3
  187. package/dist/rules/no-throw-in-detour-recover.d.ts.map +0 -1
  188. package/dist/rules/no-throw-in-detour-recover.js +0 -147
  189. package/dist/rules/no-throw-in-detour-recover.js.map +0 -1
  190. package/dist/rules/no-throw-in-detour-target.d.ts +0 -15
  191. package/dist/rules/no-throw-in-detour-target.d.ts.map +0 -1
  192. package/dist/rules/no-throw-in-detour-target.js +0 -90
  193. package/dist/rules/no-throw-in-detour-target.js.map +0 -1
  194. package/dist/rules/no-throw-in-implementation.d.ts +0 -11
  195. package/dist/rules/no-throw-in-implementation.d.ts.map +0 -1
  196. package/dist/rules/no-throw-in-implementation.js +0 -36
  197. package/dist/rules/no-throw-in-implementation.js.map +0 -1
  198. package/dist/rules/on-references-exist.d.ts +0 -14
  199. package/dist/rules/on-references-exist.d.ts.map +0 -1
  200. package/dist/rules/on-references-exist.js +0 -109
  201. package/dist/rules/on-references-exist.js.map +0 -1
  202. package/dist/rules/orphaned-signal.d.ts +0 -3
  203. package/dist/rules/orphaned-signal.d.ts.map +0 -1
  204. package/dist/rules/orphaned-signal.js +0 -67
  205. package/dist/rules/orphaned-signal.js.map +0 -1
  206. package/dist/rules/permit-governance.d.ts +0 -3
  207. package/dist/rules/permit-governance.d.ts.map +0 -1
  208. package/dist/rules/permit-governance.js +0 -15
  209. package/dist/rules/permit-governance.js.map +0 -1
  210. package/dist/rules/prefer-schema-inference.d.ts +0 -7
  211. package/dist/rules/prefer-schema-inference.d.ts.map +0 -1
  212. package/dist/rules/prefer-schema-inference.js +0 -86
  213. package/dist/rules/prefer-schema-inference.js.map +0 -1
  214. package/dist/rules/reference-exists.d.ts +0 -6
  215. package/dist/rules/reference-exists.d.ts.map +0 -1
  216. package/dist/rules/reference-exists.js +0 -47
  217. package/dist/rules/reference-exists.js.map +0 -1
  218. package/dist/rules/registry-names.d.ts +0 -8
  219. package/dist/rules/registry-names.d.ts.map +0 -1
  220. package/dist/rules/registry-names.js +0 -83
  221. package/dist/rules/registry-names.js.map +0 -1
  222. package/dist/rules/resource-declarations.d.ts +0 -14
  223. package/dist/rules/resource-declarations.d.ts.map +0 -1
  224. package/dist/rules/resource-declarations.js +0 -413
  225. package/dist/rules/resource-declarations.js.map +0 -1
  226. package/dist/rules/resource-exists.d.ts +0 -6
  227. package/dist/rules/resource-exists.d.ts.map +0 -1
  228. package/dist/rules/resource-exists.js +0 -90
  229. package/dist/rules/resource-exists.js.map +0 -1
  230. package/dist/rules/resource-id-grammar.d.ts +0 -3
  231. package/dist/rules/resource-id-grammar.d.ts.map +0 -1
  232. package/dist/rules/resource-id-grammar.js +0 -39
  233. package/dist/rules/resource-id-grammar.js.map +0 -1
  234. package/dist/rules/scan.d.ts +0 -8
  235. package/dist/rules/scan.d.ts.map +0 -1
  236. package/dist/rules/scan.js +0 -32
  237. package/dist/rules/scan.js.map +0 -1
  238. package/dist/rules/specs.d.ts +0 -29
  239. package/dist/rules/specs.d.ts.map +0 -1
  240. package/dist/rules/specs.js +0 -196
  241. package/dist/rules/specs.js.map +0 -1
  242. package/dist/rules/structure.d.ts +0 -13
  243. package/dist/rules/structure.d.ts.map +0 -1
  244. package/dist/rules/structure.js +0 -142
  245. package/dist/rules/structure.js.map +0 -1
  246. package/dist/rules/types.d.ts +0 -103
  247. package/dist/rules/types.d.ts.map +0 -1
  248. package/dist/rules/types.js +0 -2
  249. package/dist/rules/types.js.map +0 -1
  250. package/dist/rules/unreachable-detour-shadowing.d.ts +0 -3
  251. package/dist/rules/unreachable-detour-shadowing.d.ts.map +0 -1
  252. package/dist/rules/unreachable-detour-shadowing.js +0 -202
  253. package/dist/rules/unreachable-detour-shadowing.js.map +0 -1
  254. package/dist/rules/valid-describe-refs.d.ts +0 -7
  255. package/dist/rules/valid-describe-refs.d.ts.map +0 -1
  256. package/dist/rules/valid-describe-refs.js +0 -167
  257. package/dist/rules/valid-describe-refs.js.map +0 -1
  258. package/dist/rules/valid-detour-contract.d.ts +0 -3
  259. package/dist/rules/valid-detour-contract.d.ts.map +0 -1
  260. package/dist/rules/valid-detour-contract.js +0 -47
  261. package/dist/rules/valid-detour-contract.js.map +0 -1
  262. package/dist/rules/valid-detour-refs.d.ts +0 -6
  263. package/dist/rules/valid-detour-refs.d.ts.map +0 -1
  264. package/dist/rules/valid-detour-refs.js +0 -107
  265. package/dist/rules/valid-detour-refs.js.map +0 -1
  266. package/dist/rules/warden-export-symmetry.d.ts +0 -7
  267. package/dist/rules/warden-export-symmetry.d.ts.map +0 -1
  268. package/dist/rules/warden-export-symmetry.js +0 -352
  269. package/dist/rules/warden-export-symmetry.js.map +0 -1
  270. package/dist/rules/warden-rules-use-ast.d.ts +0 -17
  271. package/dist/rules/warden-rules-use-ast.d.ts.map +0 -1
  272. package/dist/rules/warden-rules-use-ast.js +0 -778
  273. package/dist/rules/warden-rules-use-ast.js.map +0 -1
  274. package/dist/trails/circular-refs.trail.d.ts +0 -24
  275. package/dist/trails/circular-refs.trail.d.ts.map +0 -1
  276. package/dist/trails/circular-refs.trail.js +0 -29
  277. package/dist/trails/circular-refs.trail.js.map +0 -1
  278. package/dist/trails/context-no-surface-types.trail.d.ts +0 -13
  279. package/dist/trails/context-no-surface-types.trail.d.ts.map +0 -1
  280. package/dist/trails/context-no-surface-types.trail.js +0 -21
  281. package/dist/trails/context-no-surface-types.trail.js.map +0 -1
  282. package/dist/trails/context-no-trailhead-types.trail.d.ts +0 -13
  283. package/dist/trails/context-no-trailhead-types.trail.d.ts.map +0 -1
  284. package/dist/trails/context-no-trailhead-types.trail.js +0 -21
  285. package/dist/trails/context-no-trailhead-types.trail.js.map +0 -1
  286. package/dist/trails/contour-exists.trail.d.ts +0 -24
  287. package/dist/trails/contour-exists.trail.d.ts.map +0 -1
  288. package/dist/trails/contour-exists.trail.js +0 -21
  289. package/dist/trails/contour-exists.trail.js.map +0 -1
  290. package/dist/trails/cross-declarations.trail.d.ts +0 -13
  291. package/dist/trails/cross-declarations.trail.d.ts.map +0 -1
  292. package/dist/trails/cross-declarations.trail.js +0 -22
  293. package/dist/trails/cross-declarations.trail.js.map +0 -1
  294. package/dist/trails/dead-internal-trail.trail.d.ts +0 -24
  295. package/dist/trails/dead-internal-trail.trail.d.ts.map +0 -1
  296. package/dist/trails/dead-internal-trail.trail.js +0 -26
  297. package/dist/trails/dead-internal-trail.trail.js.map +0 -1
  298. package/dist/trails/draft-file-marking.trail.d.ts +0 -13
  299. package/dist/trails/draft-file-marking.trail.d.ts.map +0 -1
  300. package/dist/trails/draft-file-marking.trail.js +0 -16
  301. package/dist/trails/draft-file-marking.trail.js.map +0 -1
  302. package/dist/trails/draft-visible-debt.trail.d.ts +0 -13
  303. package/dist/trails/draft-visible-debt.trail.d.ts.map +0 -1
  304. package/dist/trails/draft-visible-debt.trail.js +0 -16
  305. package/dist/trails/draft-visible-debt.trail.js.map +0 -1
  306. package/dist/trails/error-mapping-completeness.trail.d.ts +0 -13
  307. package/dist/trails/error-mapping-completeness.trail.d.ts.map +0 -1
  308. package/dist/trails/error-mapping-completeness.trail.js +0 -29
  309. package/dist/trails/error-mapping-completeness.trail.js.map +0 -1
  310. package/dist/trails/example-valid.trail.d.ts +0 -13
  311. package/dist/trails/example-valid.trail.d.ts.map +0 -1
  312. package/dist/trails/example-valid.trail.js +0 -25
  313. package/dist/trails/example-valid.trail.js.map +0 -1
  314. package/dist/trails/fires-declarations.trail.d.ts +0 -13
  315. package/dist/trails/fires-declarations.trail.d.ts.map +0 -1
  316. package/dist/trails/fires-declarations.trail.js +0 -22
  317. package/dist/trails/fires-declarations.trail.js.map +0 -1
  318. package/dist/trails/implementation-returns-result.trail.d.ts +0 -13
  319. package/dist/trails/implementation-returns-result.trail.d.ts.map +0 -1
  320. package/dist/trails/implementation-returns-result.trail.js +0 -20
  321. package/dist/trails/implementation-returns-result.trail.js.map +0 -1
  322. package/dist/trails/incomplete-accessor-for-standard-op.trail.d.ts +0 -12
  323. package/dist/trails/incomplete-accessor-for-standard-op.trail.d.ts.map +0 -1
  324. package/dist/trails/incomplete-accessor-for-standard-op.trail.js +0 -60
  325. package/dist/trails/incomplete-accessor-for-standard-op.trail.js.map +0 -1
  326. package/dist/trails/incomplete-crud.trail.d.ts +0 -24
  327. package/dist/trails/incomplete-crud.trail.d.ts.map +0 -1
  328. package/dist/trails/incomplete-crud.trail.js +0 -39
  329. package/dist/trails/incomplete-crud.trail.js.map +0 -1
  330. package/dist/trails/index.d.ts +0 -38
  331. package/dist/trails/index.d.ts.map +0 -1
  332. package/dist/trails/index.js +0 -37
  333. package/dist/trails/index.js.map +0 -1
  334. package/dist/trails/intent-propagation.trail.d.ts +0 -24
  335. package/dist/trails/intent-propagation.trail.d.ts.map +0 -1
  336. package/dist/trails/intent-propagation.trail.js +0 -30
  337. package/dist/trails/intent-propagation.trail.js.map +0 -1
  338. package/dist/trails/missing-reconcile.trail.d.ts +0 -24
  339. package/dist/trails/missing-reconcile.trail.d.ts.map +0 -1
  340. package/dist/trails/missing-reconcile.trail.js +0 -33
  341. package/dist/trails/missing-reconcile.trail.js.map +0 -1
  342. package/dist/trails/missing-visibility.trail.d.ts +0 -24
  343. package/dist/trails/missing-visibility.trail.d.ts.map +0 -1
  344. package/dist/trails/missing-visibility.trail.js +0 -22
  345. package/dist/trails/missing-visibility.trail.js.map +0 -1
  346. package/dist/trails/no-direct-impl-in-route.trail.d.ts +0 -13
  347. package/dist/trails/no-direct-impl-in-route.trail.d.ts.map +0 -1
  348. package/dist/trails/no-direct-impl-in-route.trail.js +0 -22
  349. package/dist/trails/no-direct-impl-in-route.trail.js.map +0 -1
  350. package/dist/trails/no-direct-implementation-call.trail.d.ts +0 -13
  351. package/dist/trails/no-direct-implementation-call.trail.d.ts.map +0 -1
  352. package/dist/trails/no-direct-implementation-call.trail.js +0 -16
  353. package/dist/trails/no-direct-implementation-call.trail.js.map +0 -1
  354. package/dist/trails/no-sync-result-assumption.trail.d.ts +0 -13
  355. package/dist/trails/no-sync-result-assumption.trail.d.ts.map +0 -1
  356. package/dist/trails/no-sync-result-assumption.trail.js +0 -19
  357. package/dist/trails/no-sync-result-assumption.trail.js.map +0 -1
  358. package/dist/trails/no-throw-in-detour-recover.trail.d.ts +0 -13
  359. package/dist/trails/no-throw-in-detour-recover.trail.d.ts.map +0 -1
  360. package/dist/trails/no-throw-in-detour-recover.trail.js +0 -24
  361. package/dist/trails/no-throw-in-detour-recover.trail.js.map +0 -1
  362. package/dist/trails/no-throw-in-detour-target.trail.d.ts +0 -25
  363. package/dist/trails/no-throw-in-detour-target.trail.d.ts.map +0 -1
  364. package/dist/trails/no-throw-in-detour-target.trail.js +0 -20
  365. package/dist/trails/no-throw-in-detour-target.trail.js.map +0 -1
  366. package/dist/trails/no-throw-in-implementation.trail.d.ts +0 -13
  367. package/dist/trails/no-throw-in-implementation.trail.d.ts.map +0 -1
  368. package/dist/trails/no-throw-in-implementation.trail.js +0 -20
  369. package/dist/trails/no-throw-in-implementation.trail.js.map +0 -1
  370. package/dist/trails/on-references-exist.trail.d.ts +0 -24
  371. package/dist/trails/on-references-exist.trail.d.ts.map +0 -1
  372. package/dist/trails/on-references-exist.trail.js +0 -21
  373. package/dist/trails/on-references-exist.trail.js.map +0 -1
  374. package/dist/trails/orphaned-signal.trail.d.ts +0 -24
  375. package/dist/trails/orphaned-signal.trail.d.ts.map +0 -1
  376. package/dist/trails/orphaned-signal.trail.js +0 -36
  377. package/dist/trails/orphaned-signal.trail.js.map +0 -1
  378. package/dist/trails/permit-governance.trail.d.ts +0 -12
  379. package/dist/trails/permit-governance.trail.d.ts.map +0 -1
  380. package/dist/trails/permit-governance.trail.js +0 -47
  381. package/dist/trails/permit-governance.trail.js.map +0 -1
  382. package/dist/trails/prefer-schema-inference.trail.d.ts +0 -13
  383. package/dist/trails/prefer-schema-inference.trail.d.ts.map +0 -1
  384. package/dist/trails/prefer-schema-inference.trail.js +0 -21
  385. package/dist/trails/prefer-schema-inference.trail.js.map +0 -1
  386. package/dist/trails/reference-exists.trail.d.ts +0 -24
  387. package/dist/trails/reference-exists.trail.d.ts.map +0 -1
  388. package/dist/trails/reference-exists.trail.js +0 -25
  389. package/dist/trails/reference-exists.trail.js.map +0 -1
  390. package/dist/trails/resource-declarations.trail.d.ts +0 -13
  391. package/dist/trails/resource-declarations.trail.d.ts.map +0 -1
  392. package/dist/trails/resource-declarations.trail.js +0 -25
  393. package/dist/trails/resource-declarations.trail.js.map +0 -1
  394. package/dist/trails/resource-exists.trail.d.ts +0 -24
  395. package/dist/trails/resource-exists.trail.d.ts.map +0 -1
  396. package/dist/trails/resource-exists.trail.js +0 -27
  397. package/dist/trails/resource-exists.trail.js.map +0 -1
  398. package/dist/trails/resource-id-grammar.trail.d.ts +0 -13
  399. package/dist/trails/resource-id-grammar.trail.d.ts.map +0 -1
  400. package/dist/trails/resource-id-grammar.trail.js +0 -38
  401. package/dist/trails/resource-id-grammar.trail.js.map +0 -1
  402. package/dist/trails/run.d.ts +0 -33
  403. package/dist/trails/run.d.ts.map +0 -1
  404. package/dist/trails/run.js +0 -83
  405. package/dist/trails/run.js.map +0 -1
  406. package/dist/trails/schema.d.ts +0 -78
  407. package/dist/trails/schema.d.ts.map +0 -1
  408. package/dist/trails/schema.js +0 -95
  409. package/dist/trails/schema.js.map +0 -1
  410. package/dist/trails/topo.d.ts +0 -3
  411. package/dist/trails/topo.d.ts.map +0 -1
  412. package/dist/trails/topo.js +0 -5
  413. package/dist/trails/topo.js.map +0 -1
  414. package/dist/trails/unreachable-detour-shadowing.trail.d.ts +0 -13
  415. package/dist/trails/unreachable-detour-shadowing.trail.d.ts.map +0 -1
  416. package/dist/trails/unreachable-detour-shadowing.trail.js +0 -44
  417. package/dist/trails/unreachable-detour-shadowing.trail.js.map +0 -1
  418. package/dist/trails/valid-describe-refs.trail.d.ts +0 -24
  419. package/dist/trails/valid-describe-refs.trail.d.ts.map +0 -1
  420. package/dist/trails/valid-describe-refs.trail.js +0 -18
  421. package/dist/trails/valid-describe-refs.trail.js.map +0 -1
  422. package/dist/trails/valid-detour-contract.trail.d.ts +0 -12
  423. package/dist/trails/valid-detour-contract.trail.d.ts.map +0 -1
  424. package/dist/trails/valid-detour-contract.trail.js +0 -66
  425. package/dist/trails/valid-detour-contract.trail.js.map +0 -1
  426. package/dist/trails/valid-detour-refs.trail.d.ts +0 -25
  427. package/dist/trails/valid-detour-refs.trail.d.ts.map +0 -1
  428. package/dist/trails/valid-detour-refs.trail.js +0 -24
  429. package/dist/trails/valid-detour-refs.trail.js.map +0 -1
  430. package/dist/trails/warden-export-symmetry.trail.d.ts +0 -13
  431. package/dist/trails/warden-export-symmetry.trail.d.ts.map +0 -1
  432. package/dist/trails/warden-export-symmetry.trail.js +0 -16
  433. package/dist/trails/warden-export-symmetry.trail.js.map +0 -1
  434. package/dist/trails/warden-rules-use-ast.trail.d.ts +0 -13
  435. package/dist/trails/warden-rules-use-ast.trail.d.ts.map +0 -1
  436. package/dist/trails/warden-rules-use-ast.trail.js +0 -41
  437. package/dist/trails/warden-rules-use-ast.trail.js.map +0 -1
  438. package/dist/trails/wrap-rule.d.ts +0 -43
  439. package/dist/trails/wrap-rule.d.ts.map +0 -1
  440. package/dist/trails/wrap-rule.js +0 -107
  441. package/dist/trails/wrap-rule.js.map +0 -1
  442. package/src/__tests__/ast.test.ts +0 -613
  443. package/src/__tests__/circular-refs.test.ts +0 -121
  444. package/src/__tests__/cli.test.ts +0 -526
  445. package/src/__tests__/contour-exists.test.ts +0 -203
  446. package/src/__tests__/cross-declarations.test.ts +0 -548
  447. package/src/__tests__/dead-internal-trail.test.ts +0 -81
  448. package/src/__tests__/draft-rules-context.test.ts +0 -150
  449. package/src/__tests__/drift.test.ts +0 -144
  450. package/src/__tests__/error-mapping-completeness.test.ts +0 -56
  451. package/src/__tests__/example-valid.test.ts +0 -101
  452. package/src/__tests__/fires-declarations-param-destructure.test.ts +0 -54
  453. package/src/__tests__/fires-declarations.test.ts +0 -652
  454. package/src/__tests__/formatters.test.ts +0 -157
  455. package/src/__tests__/implementation-returns-result.test.ts +0 -1143
  456. package/src/__tests__/incomplete-accessor-for-standard-op.test.ts +0 -337
  457. package/src/__tests__/incomplete-crud.test.ts +0 -498
  458. package/src/__tests__/intent-propagation.test.ts +0 -116
  459. package/src/__tests__/missing-reconcile.test.ts +0 -154
  460. package/src/__tests__/missing-visibility.test.ts +0 -108
  461. package/src/__tests__/no-direct-implementation-call.test.ts +0 -83
  462. package/src/__tests__/no-sync-result-assumption.test.ts +0 -916
  463. package/src/__tests__/no-throw-in-detour-recover.test.ts +0 -93
  464. package/src/__tests__/no-throw-in-implementation.test.ts +0 -88
  465. package/src/__tests__/on-references-exist.test.ts +0 -151
  466. package/src/__tests__/orphaned-signal.test.ts +0 -137
  467. package/src/__tests__/permit-governance.test.ts +0 -66
  468. package/src/__tests__/prefer-schema-inference.test.ts +0 -84
  469. package/src/__tests__/reference-exists.test.ts +0 -281
  470. package/src/__tests__/resource-declarations.test.ts +0 -448
  471. package/src/__tests__/resource-exists.test.ts +0 -122
  472. package/src/__tests__/resource-id-grammar.test.ts +0 -50
  473. package/src/__tests__/rules.test.ts +0 -167
  474. package/src/__tests__/topo-aware-rule.test.ts +0 -257
  475. package/src/__tests__/trails.test.ts +0 -19
  476. package/src/__tests__/unreachable-detour-shadowing.test.ts +0 -128
  477. package/src/__tests__/valid-describe-refs.test.ts +0 -243
  478. package/src/__tests__/valid-detour-contract.test.ts +0 -86
  479. package/src/__tests__/warden-export-symmetry.test.ts +0 -251
  480. package/src/__tests__/warden-rules-use-ast.test.ts +0 -468
  481. package/src/__tests__/wrap-rule.test.ts +0 -41
  482. package/src/rules/no-direct-impl-in-route.ts +0 -77
  483. package/src/trails/no-direct-impl-in-route.trail.ts +0 -22
  484. package/tsconfig.json +0 -9
  485. package/tsconfig.tests.json +0 -10
  486. package/tsconfig.tsbuildinfo +0 -1
package/src/cli.ts CHANGED
@@ -10,8 +10,24 @@ import { resolve } from 'node:path';
10
10
  import type { Topo } from '@ontrails/core';
11
11
  import { getContourReferences } from '@ontrails/core';
12
12
 
13
+ import type {
14
+ EffectiveWardenConfig,
15
+ WardenConfigInput,
16
+ WardenConfigLayer,
17
+ WardenDepth,
18
+ WardenFailOn,
19
+ WardenFormat,
20
+ WardenLockMode,
21
+ } from './config.js';
22
+ import { resolveWardenConfig } from './config.js';
23
+ import { isDraftMarkedFile } from './draft.js';
13
24
  import type { DriftResult } from './drift.js';
14
25
  import { checkDrift } from './drift.js';
26
+ import {
27
+ collectProjectDocumentationImportResolutions,
28
+ collectProjectImportResolutions,
29
+ collectPublicWorkspaces,
30
+ } from './project-context.js';
15
31
  import {
16
32
  collectContourDefinitionIds,
17
33
  collectContourReferenceTargetsByName,
@@ -27,27 +43,66 @@ import {
27
43
  } from './rules/ast.js';
28
44
  import { collectFileCrudCoverage } from './rules/incomplete-crud.js';
29
45
  import { wardenRules, wardenTopoRules } from './rules/index.js';
46
+ import { getWardenRuleMetadata } from './rules/metadata.js';
30
47
  import type {
31
48
  ProjectAwareWardenRule,
32
49
  ProjectContext,
33
50
  TopoAwareWardenRule,
34
51
  WardenDiagnostic,
52
+ WardenGuidanceLink,
35
53
  WardenRule,
54
+ WardenRuleTier,
36
55
  } from './rules/types.js';
56
+ import type { WardenImportResolution } from './resolve.js';
57
+
58
+ /**
59
+ * Resolved topo input for Warden runs that govern multiple apps.
60
+ */
61
+ export interface WardenTopoTarget {
62
+ /** Stable app/topo label used to tag topo-aware diagnostics. */
63
+ readonly name?: string | undefined;
64
+ /** Resolved topo module to inspect. */
65
+ readonly topo: Topo;
66
+ }
37
67
 
38
68
  /**
39
- * Options for the warden CLI runner.
69
+ * Options for the shared Warden runner.
40
70
  */
41
- export interface WardenOptions {
71
+ export interface WardenRunOptions {
42
72
  /** Root directory to scan for TypeScript files. Defaults to cwd. */
43
73
  readonly rootDir?: string | undefined;
74
+ /** Warden config section from `trails.config.ts`, if already loaded. */
75
+ readonly config?: WardenConfigInput | undefined;
76
+ /** CLI/config-layer app names carried through shared resolution. */
77
+ readonly apps?: readonly string[] | undefined;
78
+ /** Cumulative analysis depth for the final M1 surfaces. */
79
+ readonly depth?: WardenDepth | undefined;
80
+ /** Draft-state handling mode for final M1 surfaces. */
81
+ readonly drafts?: EffectiveWardenConfig['drafts'] | undefined;
82
+ /** Failure threshold used to compute `report.passed`. */
83
+ readonly failOn?: WardenFailOn | undefined;
84
+ /** Output format requested by the caller. */
85
+ readonly format?: WardenFormat | undefined;
86
+ /** Lockfile mode requested by the caller. */
87
+ readonly lock?: WardenLockMode | undefined;
88
+ /** Suppress lockfile mutation for CI/pre-push callers. */
89
+ readonly noLockMutation?: boolean | undefined;
90
+ /** Environment layer for config resolution. Pass `process.env` at process boundaries. */
91
+ readonly env?: Record<string, string | undefined> | undefined;
44
92
  /** Only run lint rules, skip drift detection */
45
93
  readonly lintOnly?: boolean | undefined;
46
94
  /** Only run drift detection, skip lint rules */
47
95
  readonly driftOnly?: boolean | undefined;
48
96
  /**
49
- * App topology for drift detection. When provided, enables real trailhead
50
- * lock comparison and unlocks the topo-aware rule dispatch path.
97
+ * Run a single Warden tier. Defaults to all lint tiers plus drift.
98
+ *
99
+ * Selecting a non-drift tier skips drift detection; selecting `drift` skips
100
+ * lint rule dispatch. `lintOnly` and `driftOnly` remain compatibility shims.
101
+ */
102
+ readonly tier?: WardenRuleTier | undefined;
103
+ /**
104
+ * App topology for drift detection. When provided, enables real topology
105
+ * drift comparison and unlocks the topo-aware rule dispatch path.
51
106
  *
52
107
  * @remarks
53
108
  * Topo-aware rules (both built-in `wardenTopoRules` and `extraTopoRules`)
@@ -56,6 +111,12 @@ export interface WardenOptions {
56
111
  * must pass `topo` explicitly.
57
112
  */
58
113
  readonly topo?: Topo | undefined;
114
+ /**
115
+ * Multiple resolved topos to govern in one invocation.
116
+ *
117
+ * Source/project rules run once; topo-aware rules run once per target.
118
+ */
119
+ readonly topos?: readonly WardenTopoTarget[] | undefined;
59
120
  /**
60
121
  * Extra topo-aware rules to run in addition to the built-in registry.
61
122
  *
@@ -66,6 +127,9 @@ export interface WardenOptions {
66
127
  readonly extraTopoRules?: readonly TopoAwareWardenRule[] | undefined;
67
128
  }
68
129
 
130
+ /** Backwards-compatible name for older consumers. */
131
+ export type WardenOptions = WardenRunOptions;
132
+
69
133
  /**
70
134
  * Result of a warden run.
71
135
  */
@@ -80,33 +144,138 @@ export interface WardenReport {
80
144
  readonly drift: DriftResult | null;
81
145
  /** Whether the warden run passed (no errors, no drift) */
82
146
  readonly passed: boolean;
147
+ /** Effective shared config consumed by this run. */
148
+ readonly effectiveConfig?: EffectiveWardenConfig | undefined;
149
+ /** Resolved topo/app labels governed by this run. */
150
+ readonly topoNames?: readonly string[] | undefined;
83
151
  }
84
152
 
85
153
  /**
86
- * Collect all .ts files under a directory, excluding node_modules, dist, and .git.
154
+ * Collect Warden scan targets under a directory, excluding generated and test
155
+ * surfaces that should not contribute most committed-source diagnostics.
87
156
  */
88
- const isSourceFile = (match: string): boolean =>
89
- !match.endsWith('.d.ts') &&
90
- !match.startsWith('node_modules/') &&
91
- !match.startsWith('dist/') &&
92
- !match.startsWith('.git/') &&
93
- !match.includes('__tests__/') &&
94
- !match.includes('__test__/') &&
95
- !match.endsWith('.test.ts') &&
96
- !match.endsWith('.spec.ts');
97
-
98
- const collectTsFiles = (dir: string): readonly string[] => {
157
+ const isInfrastructureScanTarget = (match: string): boolean =>
158
+ match.endsWith('.d.ts') ||
159
+ match.startsWith('node_modules/') ||
160
+ match.startsWith('dist/') ||
161
+ match.startsWith('.git/');
162
+
163
+ const isTestScanTarget = (match: string): boolean =>
164
+ match.includes('__tests__/') ||
165
+ match.includes('__test__/') ||
166
+ match.endsWith('.test.ts') ||
167
+ match.endsWith('.spec.ts');
168
+
169
+ const isAllowedScanTarget = (match: string): boolean =>
170
+ !isInfrastructureScanTarget(match) && !isTestScanTarget(match);
171
+
172
+ const isDevPermitTestScanTarget = (match: string): boolean =>
173
+ !isInfrastructureScanTarget(match) && isTestScanTarget(match);
174
+
175
+ const collectFilesMatching = (
176
+ dir: string,
177
+ pattern: string,
178
+ dot = false
179
+ ): readonly string[] => {
180
+ const glob = new Bun.Glob(pattern);
181
+ let matches: IterableIterator<string>;
182
+ try {
183
+ matches = glob.scanSync({ cwd: dir, dot, onlyFiles: true });
184
+ } catch {
185
+ return [];
186
+ }
187
+
188
+ const files: string[] = [];
189
+ for (const match of matches) {
190
+ if (isAllowedScanTarget(match)) {
191
+ files.push(`${dir}/${match}`);
192
+ }
193
+ }
194
+ return files;
195
+ };
196
+
197
+ const collectTsFiles = (dir: string): readonly string[] =>
198
+ collectFilesMatching(dir, '**/*.ts');
199
+
200
+ const draftModeIncludesFile = (
201
+ filePath: string,
202
+ drafts: EffectiveWardenConfig['drafts']
203
+ ): boolean => {
204
+ const isDraftFile = isDraftMarkedFile(filePath);
205
+ if (drafts === 'exclude') {
206
+ return !isDraftFile;
207
+ }
208
+ if (drafts === 'only') {
209
+ return isDraftFile;
210
+ }
211
+ return true;
212
+ };
213
+
214
+ const filterSourceFilesByDraftMode = (
215
+ sourceFiles: readonly SourceFile[],
216
+ drafts: EffectiveWardenConfig['drafts']
217
+ ): readonly SourceFile[] =>
218
+ drafts === 'include'
219
+ ? sourceFiles
220
+ : sourceFiles.filter((sourceFile) =>
221
+ draftModeIncludesFile(sourceFile.filePath, drafts)
222
+ );
223
+
224
+ const collectDevPermitTestFiles = (dir: string): readonly string[] => {
99
225
  const glob = new Bun.Glob('**/*.ts');
100
226
  let matches: IterableIterator<string>;
101
227
  try {
102
- matches = glob.scanSync({ cwd: dir, dot: false, onlyFiles: true });
228
+ matches = glob.scanSync({ cwd: dir, onlyFiles: true });
229
+ } catch {
230
+ return [];
231
+ }
232
+
233
+ const files: string[] = [];
234
+ for (const match of matches) {
235
+ if (isDevPermitTestScanTarget(match)) {
236
+ files.push(`${dir}/${match}`);
237
+ }
238
+ }
239
+ return files;
240
+ };
241
+
242
+ const collectTextScanFiles = (dir: string): readonly string[] => [
243
+ ...collectFilesMatching(dir, '**/*.sh', true),
244
+ ...collectFilesMatching(dir, '**/*.bash', true),
245
+ ...collectFilesMatching(dir, '**/*.zsh', true),
246
+ ...collectFilesMatching(dir, '**/*.yml', true),
247
+ ...collectFilesMatching(dir, '**/*.yaml', true),
248
+ ...collectFilesMatching(dir, '**/package.json', true),
249
+ ];
250
+
251
+ const isDocumentationScanTarget = (match: string): boolean => {
252
+ if (match === 'README.md') {
253
+ return true;
254
+ }
255
+ if (/^(?:packages|adapters|apps)\/[^/]+\/README\.md$/.test(match)) {
256
+ return true;
257
+ }
258
+ return (
259
+ match.startsWith('docs/') &&
260
+ !match.startsWith('docs/adr/') &&
261
+ !match.startsWith('docs/migration/') &&
262
+ !match.startsWith('docs/releases/') &&
263
+ match.endsWith('.md')
264
+ );
265
+ };
266
+
267
+ const collectDocumentationFiles = (dir: string): readonly string[] => {
268
+ const glob = new Bun.Glob('**/*.md');
269
+ let matches: IterableIterator<string>;
270
+ try {
271
+ matches = glob.scanSync({ cwd: dir, onlyFiles: true });
103
272
  } catch {
104
273
  return [];
105
274
  }
106
275
 
107
276
  const files: string[] = [];
108
277
  for (const match of matches) {
109
- if (isSourceFile(match)) {
278
+ if (isAllowedScanTarget(match) && isDocumentationScanTarget(match)) {
110
279
  files.push(`${dir}/${match}`);
111
280
  }
112
281
  }
@@ -115,6 +284,7 @@ const collectTsFiles = (dir: string): readonly string[] => {
115
284
 
116
285
  interface SourceFile {
117
286
  readonly filePath: string;
287
+ readonly kind: 'documentation' | 'text' | 'typescript';
118
288
  readonly sourceCode: string;
119
289
  }
120
290
 
@@ -127,7 +297,13 @@ interface MutableProjectContext {
127
297
  knownResourceIds: Set<string>;
128
298
  knownSignalIds: Set<string>;
129
299
  knownTrailIds: Set<string>;
300
+ importResolutionsByFile: Map<string, readonly WardenImportResolution[]>;
301
+ documentedImportResolutionsByFile: Map<
302
+ string,
303
+ readonly WardenImportResolution[]
304
+ >;
130
305
  onTargetSignalIds: Set<string>;
306
+ publicWorkspaces: ReturnType<typeof collectPublicWorkspaces>;
131
307
  reconcileTableIds: Set<string>;
132
308
  trailIntentsById: Map<string, 'destroy' | 'read' | 'write'>;
133
309
  }
@@ -137,11 +313,17 @@ const createMutableProjectContext = (): MutableProjectContext => ({
137
313
  crossTargetTrailIds: new Set<string>(),
138
314
  crudCoverageByEntity: new Map<string, Set<string>>(),
139
315
  crudTableIds: new Set<string>(),
316
+ documentedImportResolutionsByFile: new Map<
317
+ string,
318
+ readonly WardenImportResolution[]
319
+ >(),
320
+ importResolutionsByFile: new Map<string, readonly WardenImportResolution[]>(),
140
321
  knownContourIds: new Set<string>(),
141
322
  knownResourceIds: new Set<string>(),
142
323
  knownSignalIds: new Set<string>(),
143
324
  knownTrailIds: new Set<string>(),
144
325
  onTargetSignalIds: new Set<string>(),
326
+ publicWorkspaces: new Map(),
145
327
  reconcileTableIds: new Set<string>(),
146
328
  trailIntentsById: new Map<string, 'destroy' | 'read' | 'write'>(),
147
329
  });
@@ -192,9 +374,21 @@ const toProjectContext = (context: MutableProjectContext): ProjectContext => ({
192
374
  knownResourceIds: context.knownResourceIds,
193
375
  knownSignalIds: context.knownSignalIds,
194
376
  knownTrailIds: context.knownTrailIds,
377
+ ...(context.importResolutionsByFile.size > 0
378
+ ? { importResolutionsByFile: context.importResolutionsByFile }
379
+ : {}),
380
+ ...(context.documentedImportResolutionsByFile.size > 0
381
+ ? {
382
+ documentedImportResolutionsByFile:
383
+ context.documentedImportResolutionsByFile,
384
+ }
385
+ : {}),
195
386
  ...(context.onTargetSignalIds.size > 0
196
387
  ? { onTargetSignalIds: context.onTargetSignalIds }
197
388
  : {}),
389
+ ...(context.publicWorkspaces.size > 0
390
+ ? { publicWorkspaces: context.publicWorkspaces }
391
+ : {}),
198
392
  ...(context.reconcileTableIds.size > 0
199
393
  ? { reconcileTableIds: context.reconcileTableIds }
200
394
  : {}),
@@ -357,6 +551,43 @@ const loadSourceFiles = async (
357
551
  try {
358
552
  sourceFiles.push({
359
553
  filePath,
554
+ kind: 'typescript',
555
+ sourceCode: await Bun.file(filePath).text(),
556
+ });
557
+ } catch {
558
+ continue;
559
+ }
560
+ }
561
+
562
+ for (const filePath of collectTextScanFiles(rootDir)) {
563
+ try {
564
+ sourceFiles.push({
565
+ filePath,
566
+ kind: 'text',
567
+ sourceCode: await Bun.file(filePath).text(),
568
+ });
569
+ } catch {
570
+ continue;
571
+ }
572
+ }
573
+
574
+ for (const filePath of collectDocumentationFiles(rootDir)) {
575
+ try {
576
+ sourceFiles.push({
577
+ filePath,
578
+ kind: 'documentation',
579
+ sourceCode: await Bun.file(filePath).text(),
580
+ });
581
+ } catch {
582
+ continue;
583
+ }
584
+ }
585
+
586
+ for (const filePath of collectDevPermitTestFiles(rootDir)) {
587
+ try {
588
+ sourceFiles.push({
589
+ filePath,
590
+ kind: 'text',
360
591
  sourceCode: await Bun.file(filePath).text(),
361
592
  });
362
593
  } catch {
@@ -517,26 +748,70 @@ const collectFileContourReferences = (
517
748
  }
518
749
  };
519
750
 
751
+ const collectFileImportResolutions = (
752
+ rootDir: string,
753
+ sourceFiles: readonly SourceFile[],
754
+ context: MutableProjectContext
755
+ ): void => {
756
+ const resolutionsByFile = collectProjectImportResolutions({
757
+ rootDir,
758
+ sourceFiles,
759
+ });
760
+ for (const [filePath, resolutions] of resolutionsByFile) {
761
+ context.importResolutionsByFile.set(filePath, resolutions);
762
+ }
763
+ };
764
+
765
+ const collectFileDocumentedImportResolutions = (
766
+ rootDir: string,
767
+ sourceFiles: readonly SourceFile[],
768
+ context: MutableProjectContext
769
+ ): void => {
770
+ const resolutionsByFile = collectProjectDocumentationImportResolutions({
771
+ rootDir,
772
+ sourceFiles,
773
+ });
774
+ for (const [filePath, resolutions] of resolutionsByFile) {
775
+ context.documentedImportResolutionsByFile.set(filePath, resolutions);
776
+ }
777
+ };
778
+
520
779
  const buildProjectContext = (
521
780
  sourceFiles: readonly SourceFile[],
522
- appTopo?: Topo | undefined
781
+ rootDir: string,
782
+ appTopos: readonly Topo[] = []
523
783
  ): ProjectContext => {
524
784
  const context = createMutableProjectContext();
785
+ const typeScriptSourceFiles = sourceFiles.filter(
786
+ (sourceFile) => sourceFile.kind === 'typescript'
787
+ );
788
+ const documentationSourceFiles = sourceFiles.filter(
789
+ (sourceFile) => sourceFile.kind === 'documentation'
790
+ );
791
+ context.publicWorkspaces = collectPublicWorkspaces(rootDir);
525
792
 
526
- if (appTopo) {
527
- collectTopoTrailContext(appTopo, context);
528
- for (const sourceFile of sourceFiles) {
793
+ if (appTopos.length > 0) {
794
+ for (const appTopo of appTopos) {
795
+ collectTopoTrailContext(appTopo, context);
796
+ }
797
+ for (const sourceFile of typeScriptSourceFiles) {
529
798
  collectFileSupplementalProjectContext(sourceFile, context);
530
799
  }
531
800
  } else {
532
- for (const sourceFile of sourceFiles) {
801
+ for (const sourceFile of typeScriptSourceFiles) {
533
802
  collectFileProjectContext(sourceFile, context);
534
803
  }
535
804
  }
536
805
 
537
- for (const sourceFile of sourceFiles) {
806
+ for (const sourceFile of typeScriptSourceFiles) {
538
807
  collectFileContourReferences(sourceFile, context);
539
808
  }
809
+ collectFileImportResolutions(rootDir, typeScriptSourceFiles, context);
810
+ collectFileDocumentedImportResolutions(
811
+ rootDir,
812
+ documentationSourceFiles,
813
+ context
814
+ );
540
815
 
541
816
  return toProjectContext(context);
542
817
  };
@@ -544,6 +819,117 @@ const buildProjectContext = (
544
819
  const isProjectAwareRule = (rule: WardenRule): rule is ProjectAwareWardenRule =>
545
820
  'checkWithContext' in rule;
546
821
 
822
+ const createOptionsDiagnostic = (message: string): WardenDiagnostic => ({
823
+ filePath: '<warden-options>',
824
+ line: 1,
825
+ message,
826
+ rule: 'warden-options',
827
+ severity: 'error',
828
+ });
829
+
830
+ interface WardenRuleSelector {
831
+ readonly depth?: WardenDepth | undefined;
832
+ readonly tier?: WardenRuleTier | undefined;
833
+ }
834
+
835
+ const depthIncludesTier = (
836
+ depth: WardenDepth,
837
+ tier: WardenRuleTier
838
+ ): boolean => {
839
+ switch (depth) {
840
+ case 'source': {
841
+ return tier === 'source-static';
842
+ }
843
+ case 'project': {
844
+ return tier === 'source-static' || tier === 'project-static';
845
+ }
846
+ case 'topo': {
847
+ return (
848
+ tier === 'source-static' ||
849
+ tier === 'project-static' ||
850
+ tier === 'topo-aware'
851
+ );
852
+ }
853
+ case 'all': {
854
+ return true;
855
+ }
856
+ default: {
857
+ return false;
858
+ }
859
+ }
860
+ };
861
+
862
+ const ruleMatchesTier = (
863
+ metadata: ReturnType<typeof getWardenRuleMetadata>,
864
+ tier: WardenRuleTier | undefined
865
+ ): boolean => {
866
+ if (!tier) {
867
+ return true;
868
+ }
869
+
870
+ if (!metadata) {
871
+ return false;
872
+ }
873
+
874
+ return tier === 'advisory'
875
+ ? metadata.scope === 'advisory'
876
+ : metadata.tier === tier;
877
+ };
878
+
879
+ const ruleMatchesDepth = (
880
+ metadata: ReturnType<typeof getWardenRuleMetadata>,
881
+ depth: WardenDepth | undefined
882
+ ): boolean => {
883
+ if (!depth) {
884
+ return true;
885
+ }
886
+
887
+ if (!metadata) {
888
+ return false;
889
+ }
890
+
891
+ if (metadata.scope === 'advisory') {
892
+ return depth === 'all';
893
+ }
894
+
895
+ return depthIncludesTier(depth, metadata.tier);
896
+ };
897
+
898
+ const isSelectedRule = (
899
+ rule: WardenRule | TopoAwareWardenRule,
900
+ selector: WardenRuleSelector
901
+ ): boolean => {
902
+ const metadata = getWardenRuleMetadata(rule);
903
+ return selector.tier
904
+ ? ruleMatchesTier(metadata, selector.tier)
905
+ : ruleMatchesDepth(metadata, selector.depth);
906
+ };
907
+
908
+ const isSelectedTopoRule = (
909
+ rule: TopoAwareWardenRule,
910
+ selector: WardenRuleSelector
911
+ ): boolean => {
912
+ const metadata = getWardenRuleMetadata(rule);
913
+ if (selector.tier) {
914
+ return metadata
915
+ ? ruleMatchesTier(metadata, selector.tier)
916
+ : selector.tier === 'topo-aware';
917
+ }
918
+
919
+ return metadata ? ruleMatchesDepth(metadata, selector.depth) : true;
920
+ };
921
+
922
+ const withDiagnosticGuidance = (
923
+ diagnostic: WardenDiagnostic
924
+ ): WardenDiagnostic => {
925
+ if (diagnostic.guidance !== undefined) {
926
+ return diagnostic;
927
+ }
928
+
929
+ const guidance = getWardenRuleMetadata(diagnostic.rule)?.guidance;
930
+ return guidance === undefined ? diagnostic : { ...diagnostic, guidance };
931
+ };
932
+
547
933
  const topoRuleFailureDiagnostic = (
548
934
  rule: TopoAwareWardenRule,
549
935
  error: unknown
@@ -566,13 +952,14 @@ const topoRuleFailureDiagnostic = (
566
952
  */
567
953
  const lintTopo = async (
568
954
  appTopo: Topo,
569
- extraTopoRules: readonly TopoAwareWardenRule[]
955
+ extraTopoRules: readonly TopoAwareWardenRule[],
956
+ selector: WardenRuleSelector
570
957
  ): Promise<readonly WardenDiagnostic[]> => {
571
958
  const diagnostics: WardenDiagnostic[] = [];
572
959
  const rules: readonly TopoAwareWardenRule[] = [
573
960
  ...wardenTopoRules.values(),
574
961
  ...extraTopoRules,
575
- ];
962
+ ].filter((rule) => isSelectedTopoRule(rule, selector));
576
963
  for (const rule of rules) {
577
964
  try {
578
965
  diagnostics.push(...(await rule.checkTopo(appTopo)));
@@ -585,11 +972,31 @@ const lintTopo = async (
585
972
 
586
973
  const lintSourceFiles = (
587
974
  sourceFiles: readonly SourceFile[],
588
- context: ProjectContext
975
+ context: ProjectContext,
976
+ selector: WardenRuleSelector
589
977
  ): readonly WardenDiagnostic[] => {
590
978
  const diagnostics: WardenDiagnostic[] = [];
591
979
  for (const sourceFile of sourceFiles) {
592
980
  for (const rule of wardenRules.values()) {
981
+ if (
982
+ sourceFile.kind === 'text' &&
983
+ rule.name !== 'no-dev-permit-in-source' &&
984
+ rule.name !== 'public-internal-deep-imports'
985
+ ) {
986
+ continue;
987
+ }
988
+
989
+ if (
990
+ sourceFile.kind === 'documentation' &&
991
+ rule.name !== 'public-internal-deep-imports'
992
+ ) {
993
+ continue;
994
+ }
995
+
996
+ if (!isSelectedRule(rule, selector)) {
997
+ continue;
998
+ }
999
+
593
1000
  if (isProjectAwareRule(rule)) {
594
1001
  diagnostics.push(
595
1002
  ...rule.checkWithContext(
@@ -608,58 +1015,323 @@ const lintSourceFiles = (
608
1015
  return diagnostics;
609
1016
  };
610
1017
 
1018
+ const tagTopoDiagnostic = (
1019
+ diagnostic: WardenDiagnostic,
1020
+ topoName: string | undefined
1021
+ ): WardenDiagnostic =>
1022
+ topoName === undefined ? diagnostic : { ...diagnostic, topoName };
1023
+
1024
+ const lintTopoTargets = async (
1025
+ topoTargets: readonly WardenTopoTarget[],
1026
+ extraTopoRules: readonly TopoAwareWardenRule[],
1027
+ selector: WardenRuleSelector,
1028
+ tagDiagnostics: boolean
1029
+ ): Promise<readonly WardenDiagnostic[]> => {
1030
+ const diagnostics: WardenDiagnostic[] = [];
1031
+
1032
+ for (const target of topoTargets) {
1033
+ const topoDiagnostics = await lintTopo(
1034
+ target.topo,
1035
+ extraTopoRules,
1036
+ selector
1037
+ );
1038
+ const topoName = target.name ?? target.topo.name;
1039
+ diagnostics.push(
1040
+ ...(tagDiagnostics
1041
+ ? topoDiagnostics.map((diagnostic) =>
1042
+ tagTopoDiagnostic(diagnostic, topoName)
1043
+ )
1044
+ : topoDiagnostics)
1045
+ );
1046
+ }
1047
+
1048
+ return diagnostics;
1049
+ };
1050
+
1051
+ const selectorIncludesTopoRules = (selector: WardenRuleSelector): boolean => {
1052
+ if (selector.tier) {
1053
+ return selector.tier === 'advisory';
1054
+ }
1055
+
1056
+ return !selector.depth || depthIncludesTier(selector.depth, 'topo-aware');
1057
+ };
1058
+
611
1059
  /**
612
1060
  * Lint all files against all warden rules.
613
1061
  */
614
1062
  const lintFiles = async (
615
1063
  rootDir: string,
616
- appTopo?: Topo | undefined,
617
- extraTopoRules: readonly TopoAwareWardenRule[] = []
1064
+ drafts: EffectiveWardenConfig['drafts'],
1065
+ topoTargets: readonly WardenTopoTarget[],
1066
+ extraTopoRules: readonly TopoAwareWardenRule[],
1067
+ selector: WardenRuleSelector
618
1068
  ): Promise<WardenDiagnostic[]> => {
619
- const sourceFiles = await loadSourceFiles(rootDir);
620
- const context = buildProjectContext(sourceFiles, appTopo);
1069
+ if (selector.tier === 'topo-aware') {
1070
+ return [
1071
+ ...(await lintTopoTargets(topoTargets, extraTopoRules, selector, true)),
1072
+ ];
1073
+ }
1074
+
1075
+ const sourceFiles = filterSourceFilesByDraftMode(
1076
+ await loadSourceFiles(rootDir),
1077
+ drafts
1078
+ );
1079
+ const context = buildProjectContext(
1080
+ sourceFiles,
1081
+ rootDir,
1082
+ topoTargets.map((target) => target.topo)
1083
+ );
621
1084
  const allDiagnostics: WardenDiagnostic[] = [
622
- ...lintSourceFiles(sourceFiles, context),
1085
+ ...lintSourceFiles(sourceFiles, context, selector),
623
1086
  ];
624
1087
 
625
- if (appTopo) {
626
- allDiagnostics.push(...(await lintTopo(appTopo, extraTopoRules)));
1088
+ if (
1089
+ topoTargets.length > 0 &&
1090
+ (selector.tier === undefined || selector.tier === 'advisory') &&
1091
+ selectorIncludesTopoRules(selector)
1092
+ ) {
1093
+ allDiagnostics.push(
1094
+ ...(await lintTopoTargets(
1095
+ topoTargets,
1096
+ extraTopoRules,
1097
+ selector,
1098
+ topoTargets.length > 1
1099
+ ))
1100
+ );
627
1101
  }
628
1102
 
629
1103
  return allDiagnostics;
630
1104
  };
631
1105
 
1106
+ const topoTargetsFromOptions = (
1107
+ options: WardenRunOptions
1108
+ ): readonly WardenTopoTarget[] => {
1109
+ if (options.topos !== undefined && options.topos.length > 0) {
1110
+ return options.topos;
1111
+ }
1112
+
1113
+ return options.topo ? [{ name: options.topo.name, topo: options.topo }] : [];
1114
+ };
1115
+
1116
+ const aggregateDriftHash = (
1117
+ topoTargets: readonly WardenTopoTarget[],
1118
+ driftResults: readonly DriftResult[]
1119
+ ): string => {
1120
+ const currentHashes = new Set(
1121
+ driftResults.map((result) => result.currentHash)
1122
+ );
1123
+ const [onlyHash] = currentHashes;
1124
+ if (currentHashes.size === 1 && onlyHash !== undefined) {
1125
+ return onlyHash;
1126
+ }
1127
+
1128
+ const payload = driftResults
1129
+ .map((result, index) => {
1130
+ const target = topoTargets[index];
1131
+ return {
1132
+ currentHash: result.currentHash,
1133
+ topoName: target?.name ?? target?.topo.name ?? `topo-${String(index)}`,
1134
+ };
1135
+ })
1136
+ .toSorted((left, right) => left.topoName.localeCompare(right.topoName));
1137
+ const hasher = new Bun.CryptoHasher('sha256');
1138
+ hasher.update(JSON.stringify(payload));
1139
+ return hasher.digest('hex');
1140
+ };
1141
+
1142
+ const describeTopoDriftHash = (
1143
+ topoTargets: readonly WardenTopoTarget[],
1144
+ driftResults: readonly DriftResult[]
1145
+ ): string =>
1146
+ driftResults
1147
+ .map((result, index) => {
1148
+ const target = topoTargets[index];
1149
+ const topoName =
1150
+ target?.name ?? target?.topo.name ?? `topo-${String(index)}`;
1151
+ return `${topoName}=${result.committedHash ?? '<none>'}`;
1152
+ })
1153
+ .join(', ');
1154
+
1155
+ const checkDriftForTopoTargets = async (
1156
+ rootDir: string,
1157
+ topoTargets: readonly WardenTopoTarget[]
1158
+ ): Promise<DriftResult> => {
1159
+ if (topoTargets.length <= 1) {
1160
+ return checkDrift(rootDir, topoTargets[0]?.topo);
1161
+ }
1162
+
1163
+ const driftResults = await Promise.all(
1164
+ topoTargets.map((target) => checkDrift(rootDir, target.topo))
1165
+ );
1166
+ const committedHashes = new Set(
1167
+ driftResults.map((result) => result.committedHash)
1168
+ );
1169
+ if (committedHashes.size > 1) {
1170
+ return {
1171
+ blockedReason: `multi-topo drift expected one committed trails.lock hash but found conflicting hashes: ${describeTopoDriftHash(topoTargets, driftResults)}`,
1172
+ committedHash: null,
1173
+ currentHash: 'blocked',
1174
+ stale: true,
1175
+ };
1176
+ }
1177
+ const committedHash = driftResults[0]?.committedHash ?? null;
1178
+ const blockedReasons = driftResults.flatMap((result, index) => {
1179
+ if (result.blockedReason === undefined) {
1180
+ return [];
1181
+ }
1182
+ const target = topoTargets[index];
1183
+ const topoName =
1184
+ target?.name ?? target?.topo.name ?? `topo-${String(index)}`;
1185
+ return [`${topoName}: ${result.blockedReason}`];
1186
+ });
1187
+
1188
+ if (blockedReasons.length > 0) {
1189
+ return {
1190
+ blockedReason: blockedReasons.join('; '),
1191
+ committedHash,
1192
+ currentHash: 'blocked',
1193
+ stale: true,
1194
+ };
1195
+ }
1196
+
1197
+ const currentHash = aggregateDriftHash(topoTargets, driftResults);
1198
+ return {
1199
+ committedHash,
1200
+ currentHash,
1201
+ stale: committedHash !== null && committedHash !== currentHash,
1202
+ };
1203
+ };
1204
+
1205
+ const shouldRunLint = (options: WardenRunOptions): boolean =>
1206
+ options.tier ? options.tier !== 'drift' : !options.driftOnly;
1207
+
1208
+ const shouldRunDrift = (
1209
+ options: WardenRunOptions,
1210
+ effectiveConfig: EffectiveWardenConfig
1211
+ ): boolean => {
1212
+ if (effectiveConfig.lock === 'skip') {
1213
+ return false;
1214
+ }
1215
+
1216
+ if (options.tier) {
1217
+ return options.tier === 'drift';
1218
+ }
1219
+
1220
+ if (options.lintOnly) {
1221
+ return false;
1222
+ }
1223
+
1224
+ return options.driftOnly || effectiveConfig.depth === 'all';
1225
+ };
1226
+
1227
+ const reportPassed = ({
1228
+ drift,
1229
+ errorCount,
1230
+ failOn,
1231
+ warnCount,
1232
+ }: {
1233
+ readonly drift: DriftResult | null;
1234
+ readonly errorCount: number;
1235
+ readonly failOn: WardenFailOn;
1236
+ readonly warnCount: number;
1237
+ }): boolean =>
1238
+ errorCount === 0 &&
1239
+ (failOn === 'error' || warnCount === 0) &&
1240
+ !(drift?.stale ?? false) &&
1241
+ drift?.blockedReason === undefined;
1242
+
1243
+ const buildCliConfigLayer = (options: WardenRunOptions): WardenConfigLayer => ({
1244
+ ...(options.apps ? { apps: [...options.apps] } : {}),
1245
+ ...(options.depth ? { depth: options.depth } : {}),
1246
+ ...(options.drafts ? { drafts: options.drafts } : {}),
1247
+ ...(options.failOn ? { failOn: options.failOn } : {}),
1248
+ ...(options.format ? { format: options.format } : {}),
1249
+ ...(options.lock ? { lock: options.lock } : {}),
1250
+ ...(options.noLockMutation === undefined
1251
+ ? {}
1252
+ : { noLockMutation: options.noLockMutation }),
1253
+ });
1254
+
632
1255
  /**
633
1256
  * Run all warden checks and return a structured report.
634
1257
  */
635
1258
  export const runWarden = async (
636
- options: WardenOptions = {}
1259
+ options: WardenRunOptions = {}
637
1260
  ): Promise<WardenReport> => {
638
1261
  const rootDir = resolve(options.rootDir ?? process.cwd());
639
- const allDiagnostics = options.driftOnly
640
- ? []
641
- : await lintFiles(rootDir, options.topo, options.extraTopoRules ?? []);
642
- const drift = options.lintOnly
643
- ? null
644
- : await checkDrift(rootDir, options.topo);
1262
+ const { diagnostics: configDiagnostics, effectiveConfig } =
1263
+ resolveWardenConfig({
1264
+ cli: buildCliConfigLayer(options),
1265
+ config: options.config,
1266
+ env: options.env,
1267
+ });
1268
+ const optionDiagnostics =
1269
+ !options.tier && options.lintOnly && options.driftOnly
1270
+ ? [
1271
+ createOptionsDiagnostic(
1272
+ 'lintOnly and driftOnly cannot both be true. Use tier to select a single Warden mode.'
1273
+ ),
1274
+ ]
1275
+ : [];
1276
+ const topoTargets = topoTargetsFromOptions(options);
1277
+ const selector = {
1278
+ depth: options.tier ? undefined : effectiveConfig.depth,
1279
+ tier: options.tier,
1280
+ } satisfies WardenRuleSelector;
1281
+ const runLint = shouldRunLint(options);
1282
+ const runDrift = shouldRunDrift(options, effectiveConfig);
1283
+
1284
+ const rawDiagnostics = [
1285
+ ...configDiagnostics,
1286
+ ...optionDiagnostics,
1287
+ ...(runLint
1288
+ ? await lintFiles(
1289
+ rootDir,
1290
+ effectiveConfig.drafts,
1291
+ topoTargets,
1292
+ options.extraTopoRules ?? [],
1293
+ selector
1294
+ )
1295
+ : []),
1296
+ ];
1297
+ const allDiagnostics = rawDiagnostics.map(withDiagnosticGuidance);
1298
+ const drift = runDrift
1299
+ ? await checkDriftForTopoTargets(rootDir, topoTargets)
1300
+ : null;
645
1301
 
646
1302
  const errorCount = allDiagnostics.filter(
647
1303
  (d) => d.severity === 'error'
648
1304
  ).length;
649
1305
  const warnCount = allDiagnostics.filter((d) => d.severity === 'warn').length;
1306
+ const topoNames =
1307
+ topoTargets.length > 0
1308
+ ? topoTargets.map((target) => target.name ?? target.topo.name)
1309
+ : undefined;
650
1310
 
651
1311
  return {
652
1312
  diagnostics: allDiagnostics,
653
1313
  drift,
1314
+ effectiveConfig,
654
1315
  errorCount,
655
- passed:
656
- errorCount === 0 &&
657
- !(drift?.stale ?? false) &&
658
- drift?.blockedReason === undefined,
1316
+ passed: reportPassed({
1317
+ drift,
1318
+ errorCount,
1319
+ failOn: effectiveConfig.failOn,
1320
+ warnCount,
1321
+ }),
1322
+ ...(topoNames === undefined ? {} : { topoNames }),
659
1323
  warnCount,
660
1324
  };
661
1325
  };
662
1326
 
1327
+ const formatPlainGuidanceLink = (link: WardenGuidanceLink): string => {
1328
+ const target = link.path ?? link.url;
1329
+ if (target === undefined || target === link.label) {
1330
+ return link.label;
1331
+ }
1332
+ return `${link.label} (${target})`;
1333
+ };
1334
+
663
1335
  /**
664
1336
  * Format the lint section of the report.
665
1337
  */
@@ -677,6 +1349,25 @@ const formatLintSection = (report: WardenReport): string[] => {
677
1349
  lines.push(
678
1350
  ` ${d.filePath}:${String(d.line)} [${prefix}] ${d.rule} ${d.message}`
679
1351
  );
1352
+ if (d.guidance !== undefined) {
1353
+ lines.push(` Next: ${d.guidance.summary}`);
1354
+ for (const [index, step] of (d.guidance.steps ?? []).entries()) {
1355
+ lines.push(` ${String(index + 1)}. ${step}`);
1356
+ }
1357
+ if (d.guidance.commands !== undefined) {
1358
+ lines.push(
1359
+ ` Commands: ${d.guidance.commands.map((cmd) => `\`${cmd}\``).join(', ')}`
1360
+ );
1361
+ }
1362
+ if (d.guidance.docs !== undefined) {
1363
+ lines.push(
1364
+ ` Docs: ${d.guidance.docs.map(formatPlainGuidanceLink).join(', ')}`
1365
+ );
1366
+ }
1367
+ if (d.guidance.relatedRules !== undefined) {
1368
+ lines.push(` Related: ${d.guidance.relatedRules.join(', ')}`);
1369
+ }
1370
+ }
680
1371
  }
681
1372
 
682
1373
  return lines;
@@ -693,7 +1384,7 @@ const formatDriftSection = (drift: DriftResult | null): string[] => {
693
1384
  return [`Drift: blocked (${drift.blockedReason})`, ''];
694
1385
  }
695
1386
  const label = drift.stale
696
- ? 'Drift: trails.lock is stale (regenerate with `trails topo export`)'
1387
+ ? 'Drift: trails.lock is stale (regenerate with `trails topo compile`)'
697
1388
  : 'Drift: clean';
698
1389
  return [label, ''];
699
1390
  };
@@ -709,6 +1400,9 @@ const formatResultLine = (report: WardenReport): string => {
709
1400
  if (report.errorCount > 0) {
710
1401
  parts.push(`${report.errorCount} errors`);
711
1402
  }
1403
+ if (report.warnCount > 0 && report.effectiveConfig?.failOn === 'warning') {
1404
+ parts.push(`${report.warnCount} warnings`);
1405
+ }
712
1406
  if (report.drift?.blockedReason !== undefined) {
713
1407
  parts.push('established exports blocked');
714
1408
  } else if (report.drift?.stale) {