basalt-ui 1.26.0 → 1.28.0

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 (1069) hide show
  1. package/AGENTS.md +23 -23
  2. package/CHANGELOG.md +67 -0
  3. package/MIGRATING.md +896 -37
  4. package/README.md +33 -3
  5. package/agent/rules/basalt-batteries.md +25 -10
  6. package/agent/rules/basalt-controls.md +73 -43
  7. package/agent/rules/basalt-mantine.md +17 -6
  8. package/agent/rules/basalt-state.md +5 -0
  9. package/agent/rules/basalt-tokens.md +1 -1
  10. package/configs/oxlint-plugin.js +1286 -73
  11. package/configs/oxlint-plugin.test.ts +1338 -29
  12. package/configs/oxlint.json +12 -5
  13. package/dist/agent/coalesce.d.ts.map +1 -1
  14. package/dist/agent/coalesce.js +2 -1
  15. package/dist/agent/merge.d.ts.map +1 -1
  16. package/dist/agent/merge.js +2 -1
  17. package/dist/agent/part-list.d.ts.map +1 -1
  18. package/dist/agent/part-list.js +2 -0
  19. package/dist/agent/stick-to-bottom.d.ts +3 -6
  20. package/dist/agent/stick-to-bottom.d.ts.map +1 -1
  21. package/dist/agent/stick-to-bottom.js +6 -3
  22. package/dist/agent-chat/composer.d.ts +6 -2
  23. package/dist/agent-chat/composer.d.ts.map +1 -1
  24. package/dist/agent-chat/composer.js +49 -33
  25. package/dist/agent-chat/relative-time.d.ts +0 -9
  26. package/dist/agent-chat/relative-time.d.ts.map +1 -1
  27. package/dist/agent-chat/relative-time.js +2 -1
  28. package/dist/agent-chat/thread-detail-panel.d.ts +3 -2
  29. package/dist/agent-chat/thread-detail-panel.d.ts.map +1 -1
  30. package/dist/agent-chat/thread-detail-panel.js +8 -5
  31. package/dist/agent-chat/thread-feed-row.d.ts +2 -1
  32. package/dist/agent-chat/thread-feed-row.d.ts.map +1 -1
  33. package/dist/agent-chat/thread-feed-row.js +92 -92
  34. package/dist/agent-chat/thread-feed.d.ts +3 -2
  35. package/dist/agent-chat/thread-feed.d.ts.map +1 -1
  36. package/dist/agent-chat/thread-feed.js +8 -3
  37. package/dist/agent-chat/thread-message.d.ts +4 -3
  38. package/dist/agent-chat/thread-message.d.ts.map +1 -1
  39. package/dist/agent-chat/thread-message.js +70 -31
  40. package/dist/agent-chat/thread-outcome-card.d.ts +3 -2
  41. package/dist/agent-chat/thread-outcome-card.d.ts.map +1 -1
  42. package/dist/agent-chat/thread-outcome-card.js +9 -2
  43. package/dist/agent-chat/thread-workspace.d.ts +7 -2
  44. package/dist/agent-chat/thread-workspace.d.ts.map +1 -1
  45. package/dist/agent-chat/thread-workspace.js +62 -29
  46. package/dist/agent-chat/tool-chip.d.ts +3 -2
  47. package/dist/agent-chat/tool-chip.d.ts.map +1 -1
  48. package/dist/agent-chat/tool-chip.js +7 -2
  49. package/dist/charts/hooks/useBandPlot.d.ts.map +1 -1
  50. package/dist/charts/hooks/useBandPlot.js +15 -8
  51. package/dist/charts/index.d.ts +8 -7
  52. package/dist/charts/index.d.ts.map +1 -1
  53. package/dist/charts/index.js +45 -5
  54. package/dist/charts/kinds/BandStrip.d.ts +10 -7
  55. package/dist/charts/kinds/BandStrip.d.ts.map +1 -1
  56. package/dist/charts/kinds/BandStrip.js +12 -7
  57. package/dist/charts/kinds/Bars.d.ts +12 -6
  58. package/dist/charts/kinds/Bars.d.ts.map +1 -1
  59. package/dist/charts/kinds/Bars.js +37 -11
  60. package/dist/charts/kinds/Donut.d.ts +6 -5
  61. package/dist/charts/kinds/Donut.d.ts.map +1 -1
  62. package/dist/charts/kinds/Donut.js +19 -2
  63. package/dist/charts/kinds/DualPanel.d.ts +6 -5
  64. package/dist/charts/kinds/DualPanel.d.ts.map +1 -1
  65. package/dist/charts/kinds/DualPanel.js +36 -8
  66. package/dist/charts/kinds/Heatmap.d.ts +10 -6
  67. package/dist/charts/kinds/Heatmap.d.ts.map +1 -1
  68. package/dist/charts/kinds/Heatmap.js +58 -31
  69. package/dist/charts/kinds/MirroredBars.d.ts +6 -5
  70. package/dist/charts/kinds/MirroredBars.d.ts.map +1 -1
  71. package/dist/charts/kinds/MirroredBars.js +9 -2
  72. package/dist/charts/kinds/MultiLine.d.ts +11 -6
  73. package/dist/charts/kinds/MultiLine.d.ts.map +1 -1
  74. package/dist/charts/kinds/MultiLine.js +24 -15
  75. package/dist/charts/kinds/StackedArea.d.ts +18 -8
  76. package/dist/charts/kinds/StackedArea.d.ts.map +1 -1
  77. package/dist/charts/kinds/StackedArea.js +29 -4
  78. package/dist/charts/kinds/ZonedLine.d.ts +12 -8
  79. package/dist/charts/kinds/ZonedLine.d.ts.map +1 -1
  80. package/dist/charts/kinds/ZonedLine.js +33 -23
  81. package/dist/charts/layout/auto-margin.d.ts +65 -8
  82. package/dist/charts/layout/auto-margin.d.ts.map +1 -1
  83. package/dist/charts/layout/auto-margin.js +45 -10
  84. package/dist/charts/layout/log-ticks.d.ts +27 -0
  85. package/dist/charts/layout/log-ticks.d.ts.map +1 -0
  86. package/dist/charts/layout/log-ticks.js +42 -0
  87. package/dist/charts/primitives/Axes.d.ts +11 -2
  88. package/dist/charts/primitives/Axes.d.ts.map +1 -1
  89. package/dist/charts/primitives/Axes.js +15 -5
  90. package/dist/charts/primitives/CartesianChart.d.ts +45 -7
  91. package/dist/charts/primitives/CartesianChart.d.ts.map +1 -1
  92. package/dist/charts/primitives/CartesianChart.js +86 -28
  93. package/dist/charts/primitives/ChartCard.d.ts +27 -2
  94. package/dist/charts/primitives/ChartCard.d.ts.map +1 -1
  95. package/dist/charts/primitives/ChartCard.js +37 -17
  96. package/dist/charts/primitives/ChartFrame.d.ts +30 -4
  97. package/dist/charts/primitives/ChartFrame.d.ts.map +1 -1
  98. package/dist/charts/primitives/ChartFrame.js +45 -15
  99. package/dist/charts/primitives/ChartLegend.d.ts +13 -2
  100. package/dist/charts/primitives/ChartLegend.d.ts.map +1 -1
  101. package/dist/charts/primitives/ChartLegend.js +33 -13
  102. package/dist/charts/primitives/ChartPending.d.ts +77 -4
  103. package/dist/charts/primitives/ChartPending.d.ts.map +1 -1
  104. package/dist/charts/primitives/ChartPending.js +100 -6
  105. package/dist/charts/primitives/ChartTooltip.d.ts +7 -0
  106. package/dist/charts/primitives/ChartTooltip.d.ts.map +1 -1
  107. package/dist/charts/primitives/ChartTooltip.js +18 -5
  108. package/dist/charts/primitives/Crosshair.d.ts +3 -0
  109. package/dist/charts/primitives/Crosshair.d.ts.map +1 -1
  110. package/dist/charts/primitives/Crosshair.js +3 -1
  111. package/dist/charts/primitives/chart-frame-layout.d.ts +95 -0
  112. package/dist/charts/primitives/chart-frame-layout.d.ts.map +1 -0
  113. package/dist/charts/primitives/chart-frame-layout.js +89 -0
  114. package/dist/charts/primitives/chart-tier.d.ts +16 -0
  115. package/dist/charts/primitives/chart-tier.d.ts.map +1 -0
  116. package/dist/charts/primitives/chart-tier.js +20 -0
  117. package/dist/charts/series.d.ts +55 -1
  118. package/dist/charts/series.d.ts.map +1 -1
  119. package/dist/charts/series.js +27 -1
  120. package/dist/charts/sparklines/BarSparkline.d.ts +3 -3
  121. package/dist/charts/sparklines/BarSparkline.d.ts.map +1 -1
  122. package/dist/charts/sparklines/BarSparkline.js +9 -3
  123. package/dist/charts/sparklines/LineSparkline.d.ts +3 -3
  124. package/dist/charts/sparklines/LineSparkline.d.ts.map +1 -1
  125. package/dist/charts/sparklines/LineSparkline.js +15 -3
  126. package/dist/charts/sparklines/index.d.ts +2 -2
  127. package/dist/charts/sparklines/index.d.ts.map +1 -1
  128. package/dist/charts/utils/format.d.ts +92 -2
  129. package/dist/charts/utils/format.d.ts.map +1 -1
  130. package/dist/charts/utils/format.js +64 -1
  131. package/dist/charts/utils/ticks.d.ts +81 -2
  132. package/dist/charts/utils/ticks.d.ts.map +1 -1
  133. package/dist/charts/utils/ticks.js +54 -4
  134. package/dist/cli/index.d.ts.map +1 -1
  135. package/dist/cli/index.js +2 -2
  136. package/dist/commands/ShortcutsHelp.d.ts +3 -2
  137. package/dist/commands/ShortcutsHelp.d.ts.map +1 -1
  138. package/dist/commands/ShortcutsHelp.js +8 -2
  139. package/dist/commands/define-commands.d.ts.map +1 -1
  140. package/dist/commands/define-commands.js +2 -2
  141. package/dist/commands/define-overlays.d.ts +102 -0
  142. package/dist/commands/define-overlays.d.ts.map +1 -1
  143. package/dist/commands/define-overlays.js +102 -2
  144. package/dist/commands/index.d.ts +10 -1
  145. package/dist/commands/index.d.ts.map +1 -1
  146. package/dist/commands/index.js +3 -1
  147. package/dist/commands/overlays-mount.d.ts +13 -6
  148. package/dist/commands/overlays-mount.d.ts.map +1 -1
  149. package/dist/commands/overlays-mount.js +23 -14
  150. package/dist/common/errors.d.ts +34 -0
  151. package/dist/common/errors.d.ts.map +1 -0
  152. package/dist/common/errors.js +36 -0
  153. package/dist/common/index.d.ts +15 -0
  154. package/dist/common/index.d.ts.map +1 -0
  155. package/dist/common/index.js +29 -0
  156. package/dist/common/props.d.ts +63 -0
  157. package/dist/common/props.d.ts.map +1 -0
  158. package/dist/common/props.js +6 -0
  159. package/dist/common/query-state-like.d.ts +43 -0
  160. package/dist/common/query-state-like.d.ts.map +1 -0
  161. package/dist/common/query-state-like.js +20 -0
  162. package/dist/common/refs.d.ts +29 -0
  163. package/dist/common/refs.d.ts.map +1 -0
  164. package/dist/common/refs.js +29 -0
  165. package/dist/common/scroll-parent.d.ts +23 -0
  166. package/dist/common/scroll-parent.d.ts.map +1 -0
  167. package/dist/common/scroll-parent.js +21 -0
  168. package/dist/common/validate.d.ts +38 -0
  169. package/dist/common/validate.d.ts.map +1 -0
  170. package/dist/common/validate.js +36 -0
  171. package/dist/connectivity/connectivity-indicator.d.ts +3 -1
  172. package/dist/connectivity/connectivity-indicator.d.ts.map +1 -1
  173. package/dist/connectivity/connectivity-indicator.js +3 -1
  174. package/dist/connectivity/connectivity-provider.d.ts.map +1 -1
  175. package/dist/connectivity/connectivity-provider.js +4 -1
  176. package/dist/connectivity/index.d.ts +1 -0
  177. package/dist/connectivity/index.d.ts.map +1 -1
  178. package/dist/connectivity/types.d.ts +2 -2
  179. package/dist/connectivity/types.d.ts.map +1 -1
  180. package/dist/connectivity/use-connectivity.d.ts.map +1 -1
  181. package/dist/content/article-layout.module.css +11 -4
  182. package/dist/content/callout.d.ts +8 -4
  183. package/dist/content/callout.d.ts.map +1 -1
  184. package/dist/content/callout.js +15 -8
  185. package/dist/content/guide.d.ts +5 -4
  186. package/dist/content/guide.d.ts.map +1 -1
  187. package/dist/content/guide.js +13 -4
  188. package/dist/content/heading-anchor.d.ts +24 -2
  189. package/dist/content/heading-anchor.d.ts.map +1 -1
  190. package/dist/content/heading-anchor.js +23 -9
  191. package/dist/content/index.d.ts.map +1 -1
  192. package/dist/content/markdown.d.ts.map +1 -1
  193. package/dist/content/markdown.js +2 -0
  194. package/dist/content/prose.module.css +9 -7
  195. package/dist/content/reading-progress.d.ts +10 -4
  196. package/dist/content/reading-progress.d.ts.map +1 -1
  197. package/dist/content/reading-progress.js +26 -10
  198. package/dist/content/slug.d.ts +4 -0
  199. package/dist/content/slug.d.ts.map +1 -1
  200. package/dist/content/toc.d.ts +11 -0
  201. package/dist/content/toc.d.ts.map +1 -1
  202. package/dist/content/toc.js +23 -10
  203. package/dist/controls/actions.d.ts +49 -6
  204. package/dist/controls/actions.d.ts.map +1 -1
  205. package/dist/controls/actions.js +39 -8
  206. package/dist/controls/compare-filter.d.ts +3 -2
  207. package/dist/controls/compare-filter.d.ts.map +1 -1
  208. package/dist/controls/compare-filter.js +17 -2
  209. package/dist/controls/control-group.d.ts +3 -3
  210. package/dist/controls/control-group.d.ts.map +1 -1
  211. package/dist/controls/control-group.js +9 -2
  212. package/dist/controls/controls.module.css +169 -186
  213. package/dist/controls/controls.module.css.d.ts +9 -11
  214. package/dist/controls/enum-filter.d.ts +13 -3
  215. package/dist/controls/enum-filter.d.ts.map +1 -1
  216. package/dist/controls/enum-filter.js +32 -15
  217. package/dist/controls/filter-context.d.ts +15 -6
  218. package/dist/controls/filter-context.d.ts.map +1 -1
  219. package/dist/controls/filter-pill.d.ts +3 -3
  220. package/dist/controls/filter-pill.d.ts.map +1 -1
  221. package/dist/controls/filter-pill.js +4 -1
  222. package/dist/controls/filter-set.d.ts +3 -2
  223. package/dist/controls/filter-set.d.ts.map +1 -1
  224. package/dist/controls/filter-set.js +66 -41
  225. package/dist/controls/filter-sheet.d.ts +20 -80
  226. package/dist/controls/filter-sheet.d.ts.map +1 -1
  227. package/dist/controls/filter-sheet.js +9 -95
  228. package/dist/controls/index.d.ts +3 -0
  229. package/dist/controls/index.d.ts.map +1 -1
  230. package/dist/controls/index.js +6 -0
  231. package/dist/controls/multi-select-filter.d.ts +18 -2
  232. package/dist/controls/multi-select-filter.d.ts.map +1 -1
  233. package/dist/controls/multi-select-filter.js +124 -22
  234. package/dist/controls/number-filter.d.ts +28 -0
  235. package/dist/controls/number-filter.d.ts.map +1 -0
  236. package/dist/controls/number-filter.js +172 -0
  237. package/dist/controls/panel-row.d.ts +132 -0
  238. package/dist/controls/panel-row.d.ts.map +1 -0
  239. package/dist/controls/panel-row.js +173 -0
  240. package/dist/controls/panel-row.module.css +86 -0
  241. package/dist/controls/panel-row.module.css.d.ts +13 -0
  242. package/dist/controls/range-filter.d.ts +20 -5
  243. package/dist/controls/range-filter.d.ts.map +1 -1
  244. package/dist/controls/range-filter.js +75 -38
  245. package/dist/controls/search-filter.d.ts +3 -2
  246. package/dist/controls/search-filter.d.ts.map +1 -1
  247. package/dist/controls/search-filter.js +26 -11
  248. package/dist/controls/select-filter.d.ts +2 -1
  249. package/dist/controls/select-filter.d.ts.map +1 -1
  250. package/dist/controls/select-filter.js +4 -0
  251. package/dist/controls/slider-control.d.ts +36 -0
  252. package/dist/controls/slider-control.d.ts.map +1 -0
  253. package/dist/controls/slider-control.js +64 -0
  254. package/dist/controls/sync-button.d.ts +3 -2
  255. package/dist/controls/sync-button.d.ts.map +1 -1
  256. package/dist/controls/sync-button.js +16 -3
  257. package/dist/controls/toggle-filter.d.ts +3 -2
  258. package/dist/controls/toggle-filter.d.ts.map +1 -1
  259. package/dist/controls/toggle-filter.js +29 -12
  260. package/dist/controls/view-tabs.d.ts +3 -2
  261. package/dist/controls/view-tabs.d.ts.map +1 -1
  262. package/dist/controls/view-tabs.js +43 -11
  263. package/dist/controls-dates/date-range-picker.d.ts +1 -1
  264. package/dist/controls-dates/date-range-picker.d.ts.map +1 -1
  265. package/dist/controls-dates/date-range-picker.js +6 -1
  266. package/dist/dashboard/delta-badge.d.ts +5 -1
  267. package/dist/dashboard/delta-badge.d.ts.map +1 -1
  268. package/dist/dashboard/empty-state.d.ts +10 -3
  269. package/dist/dashboard/empty-state.d.ts.map +1 -1
  270. package/dist/dashboard/empty-state.js +37 -6
  271. package/dist/dashboard/index.d.ts +6 -4
  272. package/dist/dashboard/index.d.ts.map +1 -1
  273. package/dist/dashboard/index.js +10 -2
  274. package/dist/dashboard/query-state.d.ts +48 -27
  275. package/dist/dashboard/query-state.d.ts.map +1 -1
  276. package/dist/dashboard/query-state.js +111 -57
  277. package/dist/dashboard/settings-section.d.ts +19 -9
  278. package/dist/dashboard/settings-section.d.ts.map +1 -1
  279. package/dist/dashboard/settings-section.js +74 -25
  280. package/dist/dashboard/stat-card.d.ts +76 -19
  281. package/dist/dashboard/stat-card.d.ts.map +1 -1
  282. package/dist/dashboard/stat-card.js +78 -38
  283. package/dist/dashboard/stat-card.module.css +112 -26
  284. package/dist/dashboard/stat-card.module.css.d.ts +5 -0
  285. package/dist/dashboard/stat-group.d.ts +41 -0
  286. package/dist/dashboard/stat-group.d.ts.map +1 -0
  287. package/dist/dashboard/stat-group.js +30 -0
  288. package/dist/dashboard/stat-group.module.css +66 -0
  289. package/dist/dashboard/stat-group.module.css.d.ts +8 -0
  290. package/dist/dashboard/widget-grid.d.ts +27 -0
  291. package/dist/dashboard/widget-grid.d.ts.map +1 -0
  292. package/dist/dashboard/widget-grid.js +41 -0
  293. package/dist/dashboard/widget-grid.module.css +53 -0
  294. package/dist/dashboard/widget-grid.module.css.d.ts +9 -0
  295. package/dist/data/data-table.d.ts +151 -12
  296. package/dist/data/data-table.d.ts.map +1 -1
  297. package/dist/data/data-table.js +392 -138
  298. package/dist/data/data-table.module.css +64 -0
  299. package/dist/data/data-table.module.css.d.ts +11 -0
  300. package/dist/data/index.d.ts +2 -2
  301. package/dist/data/index.d.ts.map +1 -1
  302. package/dist/data/index.js +24 -2
  303. package/dist/data/query-branch.d.ts +28 -0
  304. package/dist/data/query-branch.d.ts.map +1 -0
  305. package/dist/data/query-branch.js +10 -0
  306. package/dist/data/table.d.ts +2 -2
  307. package/dist/data/table.d.ts.map +1 -1
  308. package/dist/data/virtual-list.d.ts +65 -6
  309. package/dist/data/virtual-list.d.ts.map +1 -1
  310. package/dist/data/virtual-list.js +89 -38
  311. package/dist/data/virtual.d.ts +1 -1
  312. package/dist/data/virtual.d.ts.map +1 -1
  313. package/dist/forms/FormErrorSummary.d.ts +4 -3
  314. package/dist/forms/FormErrorSummary.d.ts.map +1 -1
  315. package/dist/forms/FormErrorSummary.js +6 -4
  316. package/dist/forms/create-form.d.ts +2 -2
  317. package/dist/forms/field.d.ts +46 -6
  318. package/dist/forms/field.d.ts.map +1 -1
  319. package/dist/forms/field.js +10 -5
  320. package/dist/forms/form-layout.d.ts +142 -0
  321. package/dist/forms/form-layout.d.ts.map +1 -0
  322. package/dist/forms/form-layout.js +110 -0
  323. package/dist/forms/form-layout.module.css +145 -0
  324. package/dist/forms/form-layout.module.css.d.ts +20 -0
  325. package/dist/forms/form-state.d.ts +42 -0
  326. package/dist/forms/form-state.d.ts.map +1 -0
  327. package/dist/forms/form-state.js +26 -0
  328. package/dist/forms/index.d.ts +22 -6
  329. package/dist/forms/index.d.ts.map +1 -1
  330. package/dist/forms/index.js +19 -3
  331. package/dist/forms/use-field-array.d.ts +60 -0
  332. package/dist/forms/use-field-array.d.ts.map +1 -0
  333. package/dist/forms/use-field-array.js +20 -0
  334. package/dist/forms/use-form-draft.d.ts +25 -10
  335. package/dist/forms/use-form-draft.d.ts.map +1 -1
  336. package/dist/forms/use-form-draft.js +55 -18
  337. package/dist/forms/use-form-submit.d.ts +88 -0
  338. package/dist/forms/use-form-submit.d.ts.map +1 -0
  339. package/dist/forms/use-form-submit.js +63 -0
  340. package/dist/guard/index.d.ts +1 -1
  341. package/dist/guard/index.d.ts.map +1 -1
  342. package/dist/guard/index.js +27 -11
  343. package/dist/index.d.ts +10 -8
  344. package/dist/index.d.ts.map +1 -1
  345. package/dist/index.js +44 -4
  346. package/dist/nav/types.d.ts +15 -6
  347. package/dist/nav/types.d.ts.map +1 -1
  348. package/dist/notifications/bell.d.ts +3 -2
  349. package/dist/notifications/bell.d.ts.map +1 -1
  350. package/dist/notifications/bell.js +73 -56
  351. package/dist/notifications/center.d.ts +15 -4
  352. package/dist/notifications/center.d.ts.map +1 -1
  353. package/dist/notifications/center.js +5 -2
  354. package/dist/notifications/define-notifications.d.ts +56 -14
  355. package/dist/notifications/define-notifications.d.ts.map +1 -1
  356. package/dist/notifications/define-notifications.js +2 -1
  357. package/dist/notifications/index.d.ts +6 -2
  358. package/dist/notifications/index.d.ts.map +1 -1
  359. package/dist/notifications/index.js +5 -1
  360. package/dist/notifications/mount-guard.d.ts +8 -0
  361. package/dist/notifications/mount-guard.d.ts.map +1 -0
  362. package/dist/notifications/mount-guard.js +19 -0
  363. package/dist/notifications/notify.d.ts +58 -1
  364. package/dist/notifications/notify.d.ts.map +1 -1
  365. package/dist/notifications/notify.js +46 -2
  366. package/dist/notifications/overlay.d.ts.map +1 -1
  367. package/dist/notifications/overlay.js +2 -0
  368. package/dist/notifications/store.d.ts +7 -0
  369. package/dist/notifications/store.d.ts.map +1 -1
  370. package/dist/provider/index.d.ts +37 -5
  371. package/dist/provider/index.d.ts.map +1 -1
  372. package/dist/provider/index.js +66 -28
  373. package/dist/provider/lab-theme.d.ts.map +1 -1
  374. package/dist/provider/lab-theme.js +2 -1
  375. package/dist/query/devtools.d.ts.map +1 -1
  376. package/dist/query/devtools.js +1 -0
  377. package/dist/router-tanstack/field.d.ts +1 -1
  378. package/dist/router-tanstack/field.d.ts.map +1 -1
  379. package/dist/router-tanstack/index.d.ts +1 -1
  380. package/dist/router-tanstack/index.d.ts.map +1 -1
  381. package/dist/router-tanstack/search-store.d.ts +4 -1
  382. package/dist/router-tanstack/search-store.d.ts.map +1 -1
  383. package/dist/router-tanstack/search-store.js +92 -8
  384. package/dist/router-tanstack/use-nav.d.ts.map +1 -1
  385. package/dist/router-tanstack/use-nav.js +1 -1
  386. package/dist/section/index.d.ts +1 -1
  387. package/dist/section/index.d.ts.map +1 -1
  388. package/dist/section/section.d.ts +36 -2
  389. package/dist/section/section.d.ts.map +1 -1
  390. package/dist/section/section.js +49 -57
  391. package/dist/section/section.module.css +11 -0
  392. package/dist/shell/account-types.d.ts +5 -2
  393. package/dist/shell/account-types.d.ts.map +1 -1
  394. package/dist/shell/app-brand.d.ts +13 -0
  395. package/dist/shell/app-brand.d.ts.map +1 -0
  396. package/dist/shell/app-brand.js +94 -0
  397. package/dist/shell/app-brand.module.css +109 -0
  398. package/dist/shell/app-brand.module.css.d.ts +14 -0
  399. package/dist/shell/app-breadcrumbs.d.ts +4 -2
  400. package/dist/shell/app-breadcrumbs.d.ts.map +1 -1
  401. package/dist/shell/app-breadcrumbs.js +33 -22
  402. package/dist/shell/app-header.module.css +10 -5
  403. package/dist/shell/app-main.module.css +107 -0
  404. package/dist/shell/app-main.module.css.d.ts +11 -0
  405. package/dist/shell/app-mobile-nav.d.ts +3 -2
  406. package/dist/shell/app-mobile-nav.d.ts.map +1 -1
  407. package/dist/shell/app-mobile-nav.js +29 -15
  408. package/dist/shell/app-mobile-nav.module.css +48 -27
  409. package/dist/shell/app-mobile-nav.module.css.d.ts +0 -1
  410. package/dist/shell/app-sidebar-account.d.ts +2 -1
  411. package/dist/shell/app-sidebar-account.d.ts.map +1 -1
  412. package/dist/shell/app-sidebar-account.js +57 -22
  413. package/dist/shell/app-sidebar.d.ts +9 -3
  414. package/dist/shell/app-sidebar.d.ts.map +1 -1
  415. package/dist/shell/app-sidebar.js +55 -109
  416. package/dist/shell/app-sidebar.module.css +53 -96
  417. package/dist/shell/app-sidebar.module.css.d.ts +0 -5
  418. package/dist/shell/index.d.ts +14 -6
  419. package/dist/shell/index.d.ts.map +1 -1
  420. package/dist/shell/index.js +47 -12
  421. package/dist/shell/isomorphic-layout-effect.d.ts +14 -0
  422. package/dist/shell/isomorphic-layout-effect.d.ts.map +1 -0
  423. package/dist/shell/isomorphic-layout-effect.js +5 -0
  424. package/dist/shell/mobile-nav-model.d.ts.map +1 -1
  425. package/dist/shell/mobile-nav-model.js +27 -6
  426. package/dist/shell/nav-count-badge.d.ts +5 -3
  427. package/dist/shell/nav-count-badge.d.ts.map +1 -1
  428. package/dist/shell/nav-count-badge.js +3 -1
  429. package/dist/shell/page-aside.d.ts +43 -0
  430. package/dist/shell/page-aside.d.ts.map +1 -0
  431. package/dist/shell/page-aside.js +202 -0
  432. package/dist/shell/page-aside.module.css +174 -0
  433. package/dist/shell/page-aside.module.css.d.ts +14 -0
  434. package/dist/shell/page-bar.d.ts +55 -15
  435. package/dist/shell/page-bar.d.ts.map +1 -1
  436. package/dist/shell/page-bar.js +130 -41
  437. package/dist/shell/page-bar.module.css +107 -13
  438. package/dist/shell/page-bar.module.css.d.ts +4 -1
  439. package/dist/shell/sidebar-search.d.ts +4 -2
  440. package/dist/shell/sidebar-search.d.ts.map +1 -1
  441. package/dist/shell/sidebar-search.js +9 -4
  442. package/dist/shell/sidebar-search.module.css +1 -1
  443. package/dist/state/fields.d.ts +227 -34
  444. package/dist/state/fields.d.ts.map +1 -1
  445. package/dist/state/fields.js +77 -30
  446. package/dist/state/persisted-or-local.d.ts +9 -0
  447. package/dist/state/persisted-or-local.d.ts.map +1 -0
  448. package/dist/state/persisted-or-local.js +24 -0
  449. package/dist/state/persisted.d.ts +12 -0
  450. package/dist/state/persisted.d.ts.map +1 -1
  451. package/dist/state/persisted.js +36 -18
  452. package/dist/state.d.ts +1 -1
  453. package/dist/state.d.ts.map +1 -1
  454. package/dist/state.js +2 -6
  455. package/dist/surfaces.d.ts +12 -11
  456. package/dist/surfaces.d.ts.map +1 -1
  457. package/dist/surfaces.js +43 -12
  458. package/dist/theme/ctl-theme.d.ts.map +1 -1
  459. package/dist/theme/index.d.ts.map +1 -1
  460. package/dist/theme/index.js +102 -17
  461. package/dist/theme/nav-link.module.css +37 -26
  462. package/dist/theme/segmented-control.module.css +22 -0
  463. package/dist/theme/segmented-control.module.css.d.ts +1 -0
  464. package/dist/theme-lab/boot.d.ts +66 -0
  465. package/dist/theme-lab/boot.d.ts.map +1 -0
  466. package/dist/theme-lab/boot.js +82 -0
  467. package/dist/theme-lab/index.d.ts +4 -43
  468. package/dist/theme-lab/index.d.ts.map +1 -1
  469. package/dist/theme-lab/index.js +9 -69
  470. package/dist/theme-toggle/index.d.ts +3 -2
  471. package/dist/theme-toggle/index.d.ts.map +1 -1
  472. package/dist/theme-toggle/index.js +8 -2
  473. package/dist/tokens/index.d.ts +42 -0
  474. package/dist/tokens/index.d.ts.map +1 -1
  475. package/dist/tokens/index.js +35 -6
  476. package/dist/tokens/palette.d.ts +45 -25
  477. package/dist/tokens/palette.d.ts.map +1 -1
  478. package/dist/tokens/palette.js +31 -14
  479. package/dist/tokens.css +4 -5
  480. package/dist/utils/is-dev.d.ts +3 -0
  481. package/dist/utils/is-dev.d.ts.map +1 -0
  482. package/dist/utils/is-dev.js +6 -0
  483. package/dist/widget-header/delta-badge.d.ts +7 -1
  484. package/dist/widget-header/delta-badge.d.ts.map +1 -1
  485. package/dist/widget-header/delta-badge.js +8 -1
  486. package/dist/widget-header/index.d.ts +2 -2
  487. package/dist/widget-header/index.d.ts.map +1 -1
  488. package/dist/widget-header/index.js +3 -1
  489. package/dist/widget-header/widget-header.d.ts +99 -4
  490. package/dist/widget-header/widget-header.d.ts.map +1 -1
  491. package/dist/widget-header/widget-header.js +45 -18
  492. package/dist/widget-header/widget-header.module.css +87 -7
  493. package/dist/widget-header/widget-header.module.css.d.ts +1 -0
  494. package/llms.txt +6 -6
  495. package/package.json +49 -25
  496. package/src/agent/coalesce.ts +2 -1
  497. package/src/agent/merge.ts +2 -1
  498. package/src/agent/part-list.tsx +2 -0
  499. package/src/agent/stick-to-bottom.tsx +9 -11
  500. package/src/agent-chat/composer.tsx +124 -103
  501. package/src/agent-chat/relative-time.ts +2 -1
  502. package/src/agent-chat/thread-detail-panel.tsx +9 -5
  503. package/src/agent-chat/thread-feed-row.tsx +17 -9
  504. package/src/agent-chat/thread-feed.tsx +9 -3
  505. package/src/agent-chat/thread-message.tsx +48 -13
  506. package/src/agent-chat/thread-outcome-card.tsx +9 -1
  507. package/src/agent-chat/thread-workspace.tsx +72 -33
  508. package/src/agent-chat/tool-chip.tsx +8 -2
  509. package/src/charts/hooks/useBandPlot.ts +26 -8
  510. package/src/charts/index.ts +39 -4
  511. package/src/charts/kinds/BandStrip.tsx +36 -14
  512. package/src/charts/kinds/Bars.tsx +67 -16
  513. package/src/charts/kinds/Donut.tsx +30 -3
  514. package/src/charts/kinds/DualPanel.tsx +56 -10
  515. package/src/charts/kinds/Heatmap.tsx +90 -33
  516. package/src/charts/kinds/MirroredBars.tsx +21 -3
  517. package/src/charts/kinds/MultiLine.tsx +61 -18
  518. package/src/charts/kinds/StackedArea.tsx +77 -6
  519. package/src/charts/kinds/ZonedLine.tsx +71 -31
  520. package/src/charts/layout/auto-margin.ts +134 -21
  521. package/src/charts/layout/log-ticks.ts +90 -0
  522. package/src/charts/primitives/Axes.tsx +33 -5
  523. package/src/charts/primitives/CartesianChart.tsx +194 -41
  524. package/src/charts/primitives/ChartCard.tsx +66 -25
  525. package/src/charts/primitives/ChartFrame.tsx +107 -26
  526. package/src/charts/primitives/ChartLegend.tsx +43 -13
  527. package/src/charts/primitives/ChartPending.tsx +185 -7
  528. package/src/charts/primitives/ChartTooltip.tsx +37 -5
  529. package/src/charts/primitives/Crosshair.tsx +6 -3
  530. package/src/charts/primitives/chart-frame-layout.ts +212 -0
  531. package/src/charts/primitives/chart-tier.tsx +37 -0
  532. package/src/charts/series.ts +77 -1
  533. package/src/charts/sparklines/BarSparkline.tsx +11 -3
  534. package/src/charts/sparklines/LineSparkline.tsx +18 -4
  535. package/src/charts/sparklines/index.ts +2 -2
  536. package/src/charts/utils/format.ts +163 -2
  537. package/src/charts/utils/ticks.ts +149 -4
  538. package/src/cli/index.ts +8 -3
  539. package/src/commands/ShortcutsHelp.tsx +10 -3
  540. package/src/commands/define-commands.ts +2 -2
  541. package/src/commands/define-overlays.ts +216 -1
  542. package/src/commands/index.ts +12 -0
  543. package/src/commands/overlays-mount.tsx +60 -26
  544. package/src/common/errors.ts +73 -0
  545. package/src/common/index.ts +21 -0
  546. package/src/common/props.ts +71 -0
  547. package/src/common/query-state-like.ts +64 -0
  548. package/src/common/refs.ts +58 -0
  549. package/src/common/scroll-parent.ts +42 -0
  550. package/src/common/validate.ts +94 -0
  551. package/src/connectivity/connectivity-indicator.tsx +6 -1
  552. package/src/connectivity/connectivity-provider.tsx +4 -1
  553. package/src/connectivity/index.ts +1 -0
  554. package/src/connectivity/types.ts +5 -2
  555. package/src/connectivity/use-connectivity.ts +3 -2
  556. package/src/content/article-layout.module.css +11 -4
  557. package/src/content/callout.tsx +13 -7
  558. package/src/content/guide.tsx +14 -4
  559. package/src/content/heading-anchor.tsx +14 -3
  560. package/src/content/index.ts +2 -0
  561. package/src/content/markdown.tsx +2 -0
  562. package/src/content/prose.module.css +9 -7
  563. package/src/content/reading-progress.tsx +40 -15
  564. package/src/content/slug.ts +4 -0
  565. package/src/content/toc.tsx +47 -10
  566. package/src/controls/actions.tsx +89 -9
  567. package/src/controls/compare-filter.tsx +20 -3
  568. package/src/controls/control-group.tsx +11 -4
  569. package/src/controls/controls.module.css +169 -186
  570. package/src/controls/controls.module.css.d.ts +9 -11
  571. package/src/controls/enum-filter.tsx +46 -19
  572. package/src/controls/filter-context.tsx +21 -8
  573. package/src/controls/filter-pill.tsx +6 -3
  574. package/src/controls/filter-set.tsx +32 -13
  575. package/src/controls/filter-sheet.tsx +44 -194
  576. package/src/controls/index.ts +8 -0
  577. package/src/controls/multi-select-filter.tsx +179 -29
  578. package/src/controls/number-filter.tsx +329 -0
  579. package/src/controls/panel-row.module.css +86 -0
  580. package/src/controls/panel-row.module.css.d.ts +13 -0
  581. package/src/controls/panel-row.tsx +392 -0
  582. package/src/controls/range-filter.tsx +133 -43
  583. package/src/controls/search-filter.tsx +31 -13
  584. package/src/controls/select-filter.tsx +11 -1
  585. package/src/controls/slider-control.tsx +159 -0
  586. package/src/controls/sync-button.tsx +13 -3
  587. package/src/controls/toggle-filter.tsx +37 -17
  588. package/src/controls/view-tabs.tsx +63 -11
  589. package/src/controls-dates/date-range-picker.tsx +8 -1
  590. package/src/dashboard/delta-badge.tsx +5 -1
  591. package/src/dashboard/empty-state.tsx +72 -25
  592. package/src/dashboard/index.ts +19 -2
  593. package/src/dashboard/query-state.tsx +154 -84
  594. package/src/dashboard/settings-section.tsx +87 -31
  595. package/src/dashboard/stat-card.module.css +112 -26
  596. package/src/dashboard/stat-card.module.css.d.ts +5 -0
  597. package/src/dashboard/stat-card.tsx +207 -68
  598. package/src/dashboard/stat-group.module.css +66 -0
  599. package/src/dashboard/stat-group.module.css.d.ts +8 -0
  600. package/src/dashboard/stat-group.tsx +74 -0
  601. package/src/dashboard/widget-grid.module.css +53 -0
  602. package/src/dashboard/widget-grid.module.css.d.ts +9 -0
  603. package/src/dashboard/widget-grid.tsx +127 -0
  604. package/src/data/data-table.module.css +64 -0
  605. package/src/data/data-table.module.css.d.ts +11 -0
  606. package/src/data/data-table.tsx +927 -346
  607. package/src/data/index.ts +30 -2
  608. package/src/data/query-branch.ts +53 -0
  609. package/src/data/table.ts +5 -0
  610. package/src/data/virtual-list.tsx +189 -55
  611. package/src/data/virtual.ts +5 -1
  612. package/src/forms/FormErrorSummary.tsx +13 -6
  613. package/src/forms/create-form.ts +2 -2
  614. package/src/forms/field.ts +60 -11
  615. package/src/forms/form-layout.module.css +145 -0
  616. package/src/forms/form-layout.module.css.d.ts +20 -0
  617. package/src/forms/form-layout.tsx +305 -0
  618. package/src/forms/form-state.tsx +94 -0
  619. package/src/forms/index.ts +38 -7
  620. package/src/forms/use-field-array.ts +94 -0
  621. package/src/forms/use-form-draft.ts +145 -53
  622. package/src/forms/use-form-submit.ts +199 -0
  623. package/src/guard/index.ts +91 -23
  624. package/src/index.ts +79 -10
  625. package/src/nav/types.ts +15 -6
  626. package/src/notifications/bell.tsx +18 -4
  627. package/src/notifications/center.tsx +18 -5
  628. package/src/notifications/define-notifications.ts +64 -18
  629. package/src/notifications/index.ts +12 -1
  630. package/src/notifications/mount-guard.ts +41 -0
  631. package/src/notifications/notify.tsx +122 -3
  632. package/src/notifications/overlay.tsx +2 -0
  633. package/src/notifications/store.ts +7 -0
  634. package/src/provider/index.tsx +101 -13
  635. package/src/provider/lab-theme.ts +2 -1
  636. package/src/query/devtools.tsx +1 -0
  637. package/src/router-tanstack/field.ts +5 -0
  638. package/src/router-tanstack/index.ts +4 -0
  639. package/src/router-tanstack/search-store.ts +171 -12
  640. package/src/router-tanstack/use-nav.ts +18 -13
  641. package/src/section/index.ts +1 -1
  642. package/src/section/section.module.css +11 -0
  643. package/src/section/section.tsx +163 -96
  644. package/src/shell/account-types.ts +5 -2
  645. package/src/shell/app-brand.module.css +109 -0
  646. package/src/shell/app-brand.module.css.d.ts +14 -0
  647. package/src/shell/app-brand.tsx +140 -0
  648. package/src/shell/app-breadcrumbs.tsx +19 -9
  649. package/src/shell/app-header.module.css +10 -5
  650. package/src/shell/app-main.module.css +107 -0
  651. package/src/shell/app-main.module.css.d.ts +11 -0
  652. package/src/shell/app-mobile-nav.module.css +48 -27
  653. package/src/shell/app-mobile-nav.module.css.d.ts +0 -1
  654. package/src/shell/app-mobile-nav.tsx +43 -11
  655. package/src/shell/app-sidebar-account.tsx +29 -6
  656. package/src/shell/app-sidebar.module.css +53 -96
  657. package/src/shell/app-sidebar.module.css.d.ts +0 -5
  658. package/src/shell/app-sidebar.tsx +40 -113
  659. package/src/shell/index.tsx +156 -65
  660. package/src/shell/isomorphic-layout-effect.ts +14 -0
  661. package/src/shell/mobile-nav-model.ts +66 -10
  662. package/src/shell/nav-count-badge.tsx +6 -1
  663. package/src/shell/page-aside.module.css +174 -0
  664. package/src/shell/page-aside.module.css.d.ts +14 -0
  665. package/src/shell/page-aside.tsx +377 -0
  666. package/src/shell/page-bar.module.css +107 -13
  667. package/src/shell/page-bar.module.css.d.ts +4 -1
  668. package/src/shell/page-bar.tsx +294 -94
  669. package/src/shell/sidebar-search.module.css +1 -1
  670. package/src/shell/sidebar-search.tsx +13 -4
  671. package/src/state/fields.ts +360 -69
  672. package/src/state/persisted-or-local.ts +50 -0
  673. package/src/state/persisted.ts +76 -26
  674. package/src/state.ts +4 -7
  675. package/src/surfaces.ts +43 -12
  676. package/src/theme/ctl-theme.tsx +10 -0
  677. package/src/theme/index.ts +129 -16
  678. package/src/theme/nav-link.module.css +37 -26
  679. package/src/theme/segmented-control.module.css +22 -0
  680. package/src/theme/segmented-control.module.css.d.ts +1 -0
  681. package/src/theme-lab/boot.ts +140 -0
  682. package/src/theme-lab/index.tsx +21 -112
  683. package/src/theme-toggle/index.tsx +10 -3
  684. package/src/tokens/index.ts +50 -6
  685. package/src/tokens/palette.ts +58 -30
  686. package/src/utils/is-dev.ts +4 -0
  687. package/src/widget-header/delta-badge.tsx +25 -5
  688. package/src/widget-header/index.ts +10 -2
  689. package/src/widget-header/widget-header.module.css +87 -7
  690. package/src/widget-header/widget-header.module.css.d.ts +1 -0
  691. package/src/widget-header/widget-header.tsx +166 -42
  692. package/dist/agent/adapter.test.d.ts +0 -2
  693. package/dist/agent/adapter.test.d.ts.map +0 -1
  694. package/dist/agent/ai-sdk-transport.test.d.ts +0 -2
  695. package/dist/agent/ai-sdk-transport.test.d.ts.map +0 -1
  696. package/dist/agent/coalesce.test.d.ts +0 -2
  697. package/dist/agent/coalesce.test.d.ts.map +0 -1
  698. package/dist/agent/foreign.test.d.ts +0 -2
  699. package/dist/agent/foreign.test.d.ts.map +0 -1
  700. package/dist/agent/id.test.d.ts +0 -2
  701. package/dist/agent/id.test.d.ts.map +0 -1
  702. package/dist/agent/merge.test.d.ts +0 -2
  703. package/dist/agent/merge.test.d.ts.map +0 -1
  704. package/dist/agent/part-list.test.d.ts +0 -2
  705. package/dist/agent/part-list.test.d.ts.map +0 -1
  706. package/dist/agent/parts.test.d.ts +0 -2
  707. package/dist/agent/parts.test.d.ts.map +0 -1
  708. package/dist/agent/thread.test.d.ts +0 -2
  709. package/dist/agent/thread.test.d.ts.map +0 -1
  710. package/dist/agent/transport.test.d.ts +0 -2
  711. package/dist/agent/transport.test.d.ts.map +0 -1
  712. package/dist/agent/use-agent-stream.test.d.ts +0 -2
  713. package/dist/agent/use-agent-stream.test.d.ts.map +0 -1
  714. package/dist/agent/use-agent-thread-runs.foreign.test.d.ts +0 -2
  715. package/dist/agent/use-agent-thread-runs.foreign.test.d.ts.map +0 -1
  716. package/dist/agent/use-agent-thread-runs.resume.test.d.ts +0 -2
  717. package/dist/agent/use-agent-thread-runs.resume.test.d.ts.map +0 -1
  718. package/dist/agent/use-agent-thread-runs.test.d.ts +0 -2
  719. package/dist/agent/use-agent-thread-runs.test.d.ts.map +0 -1
  720. package/dist/agent/use-agent-thread-runs.wedge.test.d.ts +0 -2
  721. package/dist/agent/use-agent-thread-runs.wedge.test.d.ts.map +0 -1
  722. package/dist/agent-chat/composer.test.d.ts +0 -2
  723. package/dist/agent-chat/composer.test.d.ts.map +0 -1
  724. package/dist/agent-chat/relative-time.test.d.ts +0 -2
  725. package/dist/agent-chat/relative-time.test.d.ts.map +0 -1
  726. package/dist/agent-chat/thread-feed-row.test.d.ts +0 -2
  727. package/dist/agent-chat/thread-feed-row.test.d.ts.map +0 -1
  728. package/dist/agent-chat/thread-feed.test.d.ts +0 -2
  729. package/dist/agent-chat/thread-feed.test.d.ts.map +0 -1
  730. package/dist/agent-chat/thread-message.test.d.ts +0 -2
  731. package/dist/agent-chat/thread-message.test.d.ts.map +0 -1
  732. package/dist/agent-chat/thread-workspace.test.d.ts +0 -2
  733. package/dist/agent-chat/thread-workspace.test.d.ts.map +0 -1
  734. package/dist/agent-chat/tool-chip.test.d.ts +0 -2
  735. package/dist/agent-chat/tool-chip.test.d.ts.map +0 -1
  736. package/dist/agent-chat/virtualize.type-guard.test.d.ts +0 -2
  737. package/dist/agent-chat/virtualize.type-guard.test.d.ts.map +0 -1
  738. package/dist/charts/cursor/resolve.test.d.ts +0 -2
  739. package/dist/charts/cursor/resolve.test.d.ts.map +0 -1
  740. package/dist/charts/cursor/store.test.d.ts +0 -2
  741. package/dist/charts/cursor/store.test.d.ts.map +0 -1
  742. package/dist/charts/kinds/BandStrip.test.d.ts +0 -2
  743. package/dist/charts/kinds/BandStrip.test.d.ts.map +0 -1
  744. package/dist/charts/kinds/Bars.test.d.ts +0 -2
  745. package/dist/charts/kinds/Bars.test.d.ts.map +0 -1
  746. package/dist/charts/kinds/DualPanel.test.d.ts +0 -2
  747. package/dist/charts/kinds/DualPanel.test.d.ts.map +0 -1
  748. package/dist/charts/kinds/MirroredBars.test.d.ts +0 -2
  749. package/dist/charts/kinds/MirroredBars.test.d.ts.map +0 -1
  750. package/dist/charts/kinds/MultiLine.test.d.ts +0 -2
  751. package/dist/charts/kinds/MultiLine.test.d.ts.map +0 -1
  752. package/dist/charts/kinds/StackedArea.test.d.ts +0 -2
  753. package/dist/charts/kinds/StackedArea.test.d.ts.map +0 -1
  754. package/dist/charts/kinds/ZonedLine.test.d.ts +0 -2
  755. package/dist/charts/kinds/ZonedLine.test.d.ts.map +0 -1
  756. package/dist/charts/layout/auto-margin.test.d.ts +0 -2
  757. package/dist/charts/layout/auto-margin.test.d.ts.map +0 -1
  758. package/dist/charts/primitives/CartesianChart.test.d.ts +0 -2
  759. package/dist/charts/primitives/CartesianChart.test.d.ts.map +0 -1
  760. package/dist/charts/primitives/ChartCard.test.d.ts +0 -2
  761. package/dist/charts/primitives/ChartCard.test.d.ts.map +0 -1
  762. package/dist/charts/primitives/ChartFrame.test.d.ts +0 -2
  763. package/dist/charts/primitives/ChartFrame.test.d.ts.map +0 -1
  764. package/dist/charts/primitives/ChartLegend.test.d.ts +0 -2
  765. package/dist/charts/primitives/ChartLegend.test.d.ts.map +0 -1
  766. package/dist/charts/primitives/ChartTooltip.test.d.ts +0 -2
  767. package/dist/charts/primitives/ChartTooltip.test.d.ts.map +0 -1
  768. package/dist/charts/primitives/XZoneRects.test.d.ts +0 -2
  769. package/dist/charts/primitives/XZoneRects.test.d.ts.map +0 -1
  770. package/dist/charts/series.test.d.ts +0 -2
  771. package/dist/charts/series.test.d.ts.map +0 -1
  772. package/dist/charts/utils/domain.test.d.ts +0 -2
  773. package/dist/charts/utils/domain.test.d.ts.map +0 -1
  774. package/dist/charts/utils/measure-text.test.d.ts +0 -2
  775. package/dist/charts/utils/measure-text.test.d.ts.map +0 -1
  776. package/dist/cli/check-coverage.test.d.ts +0 -2
  777. package/dist/cli/check-coverage.test.d.ts.map +0 -1
  778. package/dist/cli/check-theme.test.d.ts +0 -2
  779. package/dist/cli/check-theme.test.d.ts.map +0 -1
  780. package/dist/cli/help.test.d.ts +0 -2
  781. package/dist/cli/help.test.d.ts.map +0 -1
  782. package/dist/cli/init-sync.test.d.ts +0 -2
  783. package/dist/cli/init-sync.test.d.ts.map +0 -1
  784. package/dist/cli/placement-engine.test.d.ts +0 -2
  785. package/dist/cli/placement-engine.test.d.ts.map +0 -1
  786. package/dist/cli/shipped-versions.test.d.ts +0 -2
  787. package/dist/cli/shipped-versions.test.d.ts.map +0 -1
  788. package/dist/cli/tarball-parity.test.d.ts +0 -2
  789. package/dist/cli/tarball-parity.test.d.ts.map +0 -1
  790. package/dist/cli/template-format.test.d.ts +0 -2
  791. package/dist/cli/template-format.test.d.ts.map +0 -1
  792. package/dist/cli/template-ledger.test.d.ts +0 -2
  793. package/dist/cli/template-ledger.test.d.ts.map +0 -1
  794. package/dist/cli/tokens-css.test.d.ts +0 -2
  795. package/dist/cli/tokens-css.test.d.ts.map +0 -1
  796. package/dist/cli/toolchain-wiring.test.d.ts +0 -2
  797. package/dist/cli/toolchain-wiring.test.d.ts.map +0 -1
  798. package/dist/cli/waiver-audit.test.d.ts +0 -2
  799. package/dist/cli/waiver-audit.test.d.ts.map +0 -1
  800. package/dist/content/article-actions.test.d.ts +0 -2
  801. package/dist/content/article-actions.test.d.ts.map +0 -1
  802. package/dist/content/article-model.test.d.ts +0 -2
  803. package/dist/content/article-model.test.d.ts.map +0 -1
  804. package/dist/content/block-split.test.d.ts +0 -2
  805. package/dist/content/block-split.test.d.ts.map +0 -1
  806. package/dist/content/heading-components.test.d.ts +0 -2
  807. package/dist/content/heading-components.test.d.ts.map +0 -1
  808. package/dist/content/markdown.test.d.ts +0 -2
  809. package/dist/content/markdown.test.d.ts.map +0 -1
  810. package/dist/content/prose.module.css.test.d.ts +0 -2
  811. package/dist/content/prose.module.css.test.d.ts.map +0 -1
  812. package/dist/content/sanitize.test.d.ts +0 -2
  813. package/dist/content/sanitize.test.d.ts.map +0 -1
  814. package/dist/content/slug.test.d.ts +0 -2
  815. package/dist/content/slug.test.d.ts.map +0 -1
  816. package/dist/controls/actions.test.d.ts +0 -2
  817. package/dist/controls/actions.test.d.ts.map +0 -1
  818. package/dist/controls/actions.type-guard.test.d.ts +0 -2
  819. package/dist/controls/actions.type-guard.test.d.ts.map +0 -1
  820. package/dist/controls/control-group.test.d.ts +0 -2
  821. package/dist/controls/control-group.test.d.ts.map +0 -1
  822. package/dist/controls/controls.router.test.d.ts +0 -2
  823. package/dist/controls/controls.router.test.d.ts.map +0 -1
  824. package/dist/controls/filter-set.test.d.ts +0 -2
  825. package/dist/controls/filter-set.test.d.ts.map +0 -1
  826. package/dist/controls/peer-boundary.test.d.ts +0 -2
  827. package/dist/controls/peer-boundary.test.d.ts.map +0 -1
  828. package/dist/controls/props-contract.test.d.ts +0 -2
  829. package/dist/controls/props-contract.test.d.ts.map +0 -1
  830. package/dist/controls/sync-button.test.d.ts +0 -2
  831. package/dist/controls/sync-button.test.d.ts.map +0 -1
  832. package/dist/dashboard/query-state.test.d.ts +0 -2
  833. package/dist/dashboard/query-state.test.d.ts.map +0 -1
  834. package/dist/dashboard/settings-section.test.d.ts +0 -2
  835. package/dist/dashboard/settings-section.test.d.ts.map +0 -1
  836. package/dist/dashboard/stat-card.test.d.ts +0 -2
  837. package/dist/dashboard/stat-card.test.d.ts.map +0 -1
  838. package/dist/data/data-table.test.d.ts +0 -2
  839. package/dist/data/data-table.test.d.ts.map +0 -1
  840. package/dist/guard/check-source.test.d.ts +0 -2
  841. package/dist/guard/check-source.test.d.ts.map +0 -1
  842. package/dist/guard/grace.test.d.ts +0 -20
  843. package/dist/guard/grace.test.d.ts.map +0 -1
  844. package/dist/guard/guard-hook.test.d.ts +0 -2
  845. package/dist/guard/guard-hook.test.d.ts.map +0 -1
  846. package/dist/provider/index.test.d.ts +0 -2
  847. package/dist/provider/index.test.d.ts.map +0 -1
  848. package/dist/provider/lab-theme.test.d.ts +0 -2
  849. package/dist/provider/lab-theme.test.d.ts.map +0 -1
  850. package/dist/query/error-message.test.d.ts +0 -2
  851. package/dist/query/error-message.test.d.ts.map +0 -1
  852. package/dist/router-tanstack/multi-search-param-store.test.d.ts +0 -2
  853. package/dist/router-tanstack/multi-search-param-store.test.d.ts.map +0 -1
  854. package/dist/router-tanstack/nav.test.d.ts +0 -2
  855. package/dist/router-tanstack/nav.test.d.ts.map +0 -1
  856. package/dist/router-tanstack/search-param-store.test.d.ts +0 -2
  857. package/dist/router-tanstack/search-param-store.test.d.ts.map +0 -1
  858. package/dist/router-tanstack/search-store.router.test.d.ts +0 -2
  859. package/dist/router-tanstack/search-store.router.test.d.ts.map +0 -1
  860. package/dist/router-tanstack/search-store.test.d.ts +0 -2
  861. package/dist/router-tanstack/search-store.test.d.ts.map +0 -1
  862. package/dist/router-tanstack/use-nav.test.d.ts +0 -2
  863. package/dist/router-tanstack/use-nav.test.d.ts.map +0 -1
  864. package/dist/section/section.test.d.ts +0 -2
  865. package/dist/section/section.test.d.ts.map +0 -1
  866. package/dist/shell/app-mobile-nav.test.d.ts +0 -2
  867. package/dist/shell/app-mobile-nav.test.d.ts.map +0 -1
  868. package/dist/shell/app-sidebar.test.d.ts +0 -2
  869. package/dist/shell/app-sidebar.test.d.ts.map +0 -1
  870. package/dist/shell/index.test.d.ts +0 -2
  871. package/dist/shell/index.test.d.ts.map +0 -1
  872. package/dist/shell/mobile-nav-model.test.d.ts +0 -2
  873. package/dist/shell/mobile-nav-model.test.d.ts.map +0 -1
  874. package/dist/shell/page-bar.test.d.ts +0 -2
  875. package/dist/shell/page-bar.test.d.ts.map +0 -1
  876. package/dist/state/fields.test.d.ts +0 -2
  877. package/dist/state/fields.test.d.ts.map +0 -1
  878. package/dist/styles.floor.test.d.ts +0 -2
  879. package/dist/styles.floor.test.d.ts.map +0 -1
  880. package/dist/theme/border-coverage.test.d.ts +0 -18
  881. package/dist/theme/border-coverage.test.d.ts.map +0 -1
  882. package/dist/theme/contrast.test.d.ts +0 -2
  883. package/dist/theme/contrast.test.d.ts.map +0 -1
  884. package/dist/theme/ctl-theme.test.d.ts +0 -2
  885. package/dist/theme/ctl-theme.test.d.ts.map +0 -1
  886. package/dist/theme/ctl-tier-coverage.test.d.ts +0 -2
  887. package/dist/theme/ctl-tier-coverage.test.d.ts.map +0 -1
  888. package/dist/theme/density-option.test.d.ts +0 -2
  889. package/dist/theme/density-option.test.d.ts.map +0 -1
  890. package/dist/theme/density-relations.test.d.ts +0 -2
  891. package/dist/theme/density-relations.test.d.ts.map +0 -1
  892. package/dist/theme/derive-theme.test.d.ts +0 -2
  893. package/dist/theme/derive-theme.test.d.ts.map +0 -1
  894. package/dist/theme/divider-law.test.d.ts +0 -2
  895. package/dist/theme/divider-law.test.d.ts.map +0 -1
  896. package/dist/theme/fonts-option.test.d.ts +0 -2
  897. package/dist/theme/fonts-option.test.d.ts.map +0 -1
  898. package/dist/theme/icon-slot.test.d.ts +0 -2
  899. package/dist/theme/icon-slot.test.d.ts.map +0 -1
  900. package/dist/theme/layout-rhythm-css.test.d.ts +0 -2
  901. package/dist/theme/layout-rhythm-css.test.d.ts.map +0 -1
  902. package/dist/theme/radius-option.test.d.ts +0 -2
  903. package/dist/theme/radius-option.test.d.ts.map +0 -1
  904. package/dist/theme/radius.test.d.ts +0 -2
  905. package/dist/theme/radius.test.d.ts.map +0 -1
  906. package/dist/theme/shadow-surfaces.test.d.ts +0 -26
  907. package/dist/theme/shadow-surfaces.test.d.ts.map +0 -1
  908. package/dist/theme/spacing.test.d.ts +0 -2
  909. package/dist/theme/spacing.test.d.ts.map +0 -1
  910. package/dist/theme/use-basalt-spacing.test.d.ts +0 -2
  911. package/dist/theme/use-basalt-spacing.test.d.ts.map +0 -1
  912. package/dist/theme-lab/derive-controls.test.d.ts +0 -2
  913. package/dist/theme-lab/derive-controls.test.d.ts.map +0 -1
  914. package/dist/theme-lab/derive-state.test.d.ts +0 -2
  915. package/dist/theme-lab/derive-state.test.d.ts.map +0 -1
  916. package/dist/theme-lab/theme-lab.test.d.ts +0 -2
  917. package/dist/theme-lab/theme-lab.test.d.ts.map +0 -1
  918. package/dist/tokens/build-fonts-css.test.d.ts +0 -2
  919. package/dist/tokens/build-fonts-css.test.d.ts.map +0 -1
  920. package/dist/tokens/build-radius-css.test.d.ts +0 -2
  921. package/dist/tokens/build-radius-css.test.d.ts.map +0 -1
  922. package/dist/tokens/chart-margin.test.d.ts +0 -2
  923. package/dist/tokens/chart-margin.test.d.ts.map +0 -1
  924. package/dist/tokens/density.test.d.ts +0 -2
  925. package/dist/tokens/density.test.d.ts.map +0 -1
  926. package/dist/tokens/derive-radius.test.d.ts +0 -2
  927. package/dist/tokens/derive-radius.test.d.ts.map +0 -1
  928. package/dist/tokens/derive.test.d.ts +0 -2
  929. package/dist/tokens/derive.test.d.ts.map +0 -1
  930. package/dist/tokens/palette.test.d.ts +0 -2
  931. package/dist/tokens/palette.test.d.ts.map +0 -1
  932. package/dist/tokens/text-scale.test.d.ts +0 -2
  933. package/dist/tokens/text-scale.test.d.ts.map +0 -1
  934. package/dist/vite-color-mix.test.d.ts +0 -2
  935. package/dist/vite-color-mix.test.d.ts.map +0 -1
  936. package/dist/vite.test.d.ts +0 -2
  937. package/dist/vite.test.d.ts.map +0 -1
  938. package/dist/widget-header/delta-badge.test.d.ts +0 -2
  939. package/dist/widget-header/delta-badge.test.d.ts.map +0 -1
  940. package/dist/widget-header/no-mantine.test.d.ts +0 -2
  941. package/dist/widget-header/no-mantine.test.d.ts.map +0 -1
  942. package/dist/widget-header/widget-header.test.d.ts +0 -2
  943. package/dist/widget-header/widget-header.test.d.ts.map +0 -1
  944. package/src/agent/adapter.test.ts +0 -1640
  945. package/src/agent/ai-sdk-transport.test.ts +0 -658
  946. package/src/agent/coalesce.test.ts +0 -387
  947. package/src/agent/foreign.test.ts +0 -51
  948. package/src/agent/id.test.ts +0 -169
  949. package/src/agent/merge.test.ts +0 -206
  950. package/src/agent/part-list.test.tsx +0 -112
  951. package/src/agent/parts.test.ts +0 -720
  952. package/src/agent/thread.test.ts +0 -220
  953. package/src/agent/transport.test.ts +0 -70
  954. package/src/agent/use-agent-stream.test.tsx +0 -287
  955. package/src/agent/use-agent-thread-runs.foreign.test.tsx +0 -138
  956. package/src/agent/use-agent-thread-runs.resume.test.tsx +0 -294
  957. package/src/agent/use-agent-thread-runs.test.tsx +0 -1120
  958. package/src/agent/use-agent-thread-runs.wedge.test.tsx +0 -387
  959. package/src/agent-chat/composer.test.tsx +0 -924
  960. package/src/agent-chat/relative-time.test.ts +0 -95
  961. package/src/agent-chat/thread-feed-row.test.tsx +0 -510
  962. package/src/agent-chat/thread-feed.test.tsx +0 -241
  963. package/src/agent-chat/thread-message.test.tsx +0 -1285
  964. package/src/agent-chat/thread-workspace.test.tsx +0 -169
  965. package/src/agent-chat/tool-chip.test.tsx +0 -139
  966. package/src/agent-chat/virtualize.type-guard.test.ts +0 -49
  967. package/src/charts/cursor/resolve.test.ts +0 -151
  968. package/src/charts/cursor/store.test.ts +0 -64
  969. package/src/charts/kinds/BandStrip.test.tsx +0 -282
  970. package/src/charts/kinds/Bars.test.tsx +0 -183
  971. package/src/charts/kinds/DualPanel.test.tsx +0 -527
  972. package/src/charts/kinds/MirroredBars.test.tsx +0 -232
  973. package/src/charts/kinds/MultiLine.test.tsx +0 -218
  974. package/src/charts/kinds/StackedArea.test.tsx +0 -114
  975. package/src/charts/kinds/ZonedLine.test.tsx +0 -147
  976. package/src/charts/layout/auto-margin.test.ts +0 -42
  977. package/src/charts/primitives/CartesianChart.test.tsx +0 -647
  978. package/src/charts/primitives/ChartCard.test.tsx +0 -66
  979. package/src/charts/primitives/ChartFrame.test.tsx +0 -142
  980. package/src/charts/primitives/ChartLegend.test.tsx +0 -78
  981. package/src/charts/primitives/ChartTooltip.test.tsx +0 -44
  982. package/src/charts/primitives/XZoneRects.test.tsx +0 -123
  983. package/src/charts/series.test.ts +0 -112
  984. package/src/charts/utils/domain.test.ts +0 -54
  985. package/src/charts/utils/measure-text.test.ts +0 -36
  986. package/src/cli/check-coverage.test.ts +0 -297
  987. package/src/cli/check-theme.test.ts +0 -837
  988. package/src/cli/help.test.ts +0 -170
  989. package/src/cli/init-sync.test.ts +0 -343
  990. package/src/cli/placement-engine.test.ts +0 -298
  991. package/src/cli/shipped-versions.test.ts +0 -83
  992. package/src/cli/tarball-parity.test.ts +0 -44
  993. package/src/cli/template-format.test.ts +0 -64
  994. package/src/cli/template-ledger.test.ts +0 -219
  995. package/src/cli/tokens-css.test.ts +0 -249
  996. package/src/cli/toolchain-wiring.test.ts +0 -1358
  997. package/src/cli/waiver-audit.test.ts +0 -215
  998. package/src/content/article-actions.test.ts +0 -78
  999. package/src/content/article-model.test.ts +0 -119
  1000. package/src/content/block-split.test.ts +0 -75
  1001. package/src/content/heading-components.test.tsx +0 -65
  1002. package/src/content/markdown.test.tsx +0 -603
  1003. package/src/content/prose.module.css.test.ts +0 -39
  1004. package/src/content/sanitize.test.ts +0 -659
  1005. package/src/content/slug.test.ts +0 -62
  1006. package/src/controls/actions.test.tsx +0 -252
  1007. package/src/controls/actions.type-guard.test.ts +0 -63
  1008. package/src/controls/control-group.test.tsx +0 -229
  1009. package/src/controls/controls.router.test.tsx +0 -637
  1010. package/src/controls/filter-set.test.tsx +0 -472
  1011. package/src/controls/peer-boundary.test.ts +0 -85
  1012. package/src/controls/props-contract.test.tsx +0 -106
  1013. package/src/controls/sync-button.test.tsx +0 -183
  1014. package/src/dashboard/query-state.test.tsx +0 -152
  1015. package/src/dashboard/settings-section.test.tsx +0 -56
  1016. package/src/dashboard/stat-card.test.tsx +0 -154
  1017. package/src/data/data-table.test.tsx +0 -434
  1018. package/src/guard/check-source.test.ts +0 -3355
  1019. package/src/guard/grace.test.ts +0 -149
  1020. package/src/guard/guard-hook.test.ts +0 -203
  1021. package/src/provider/index.test.tsx +0 -101
  1022. package/src/provider/lab-theme.test.ts +0 -147
  1023. package/src/query/error-message.test.ts +0 -67
  1024. package/src/router-tanstack/multi-search-param-store.test.ts +0 -124
  1025. package/src/router-tanstack/nav.test.ts +0 -111
  1026. package/src/router-tanstack/search-param-store.test.ts +0 -135
  1027. package/src/router-tanstack/search-store.router.test.tsx +0 -661
  1028. package/src/router-tanstack/search-store.test.ts +0 -418
  1029. package/src/router-tanstack/use-nav.test.tsx +0 -187
  1030. package/src/section/section.test.tsx +0 -204
  1031. package/src/shell/app-mobile-nav.test.tsx +0 -403
  1032. package/src/shell/app-sidebar.test.tsx +0 -509
  1033. package/src/shell/index.test.tsx +0 -428
  1034. package/src/shell/mobile-nav-model.test.ts +0 -517
  1035. package/src/shell/page-bar.test.tsx +0 -316
  1036. package/src/state/fields.test.tsx +0 -258
  1037. package/src/styles.floor.test.ts +0 -36
  1038. package/src/theme/border-coverage.test.ts +0 -272
  1039. package/src/theme/contrast.test.ts +0 -354
  1040. package/src/theme/ctl-theme.test.tsx +0 -275
  1041. package/src/theme/ctl-tier-coverage.test.ts +0 -170
  1042. package/src/theme/density-option.test.ts +0 -96
  1043. package/src/theme/density-relations.test.ts +0 -506
  1044. package/src/theme/derive-theme.test.ts +0 -101
  1045. package/src/theme/divider-law.test.ts +0 -259
  1046. package/src/theme/fonts-option.test.ts +0 -75
  1047. package/src/theme/icon-slot.test.tsx +0 -275
  1048. package/src/theme/layout-rhythm-css.test.ts +0 -72
  1049. package/src/theme/radius-option.test.ts +0 -86
  1050. package/src/theme/radius.test.ts +0 -142
  1051. package/src/theme/shadow-surfaces.test.ts +0 -488
  1052. package/src/theme/spacing.test.ts +0 -495
  1053. package/src/theme/use-basalt-spacing.test.tsx +0 -55
  1054. package/src/theme-lab/derive-controls.test.ts +0 -94
  1055. package/src/theme-lab/derive-state.test.ts +0 -71
  1056. package/src/theme-lab/theme-lab.test.ts +0 -46
  1057. package/src/tokens/build-fonts-css.test.ts +0 -52
  1058. package/src/tokens/build-radius-css.test.ts +0 -25
  1059. package/src/tokens/chart-margin.test.ts +0 -39
  1060. package/src/tokens/density.test.ts +0 -259
  1061. package/src/tokens/derive-radius.test.ts +0 -62
  1062. package/src/tokens/derive.test.ts +0 -30
  1063. package/src/tokens/palette.test.ts +0 -114
  1064. package/src/tokens/text-scale.test.ts +0 -51
  1065. package/src/vite-color-mix.test.ts +0 -46
  1066. package/src/vite.test.ts +0 -423
  1067. package/src/widget-header/delta-badge.test.tsx +0 -60
  1068. package/src/widget-header/no-mantine.test.ts +0 -33
  1069. package/src/widget-header/widget-header.test.tsx +0 -187
@@ -14,6 +14,7 @@ import { dirname, resolve } from 'node:path'
14
14
  // eslint-disable-next-line -- the plugin is plain JS; the ledger + id set are named exports beside it
15
15
  import basaltPlugin, {
16
16
  CTL_THEME_TAGS,
17
+ DEPRECATED_EXPORTS,
17
18
  KNOWN_RULE_IDS,
18
19
  PLUGIN_RULE_ADVISORY,
19
20
  PLUGIN_RULE_GRACE,
@@ -35,7 +36,14 @@ beforeEach(() => {
35
36
  plugins: [],
36
37
  jsPlugins: [PLUGIN_PATH],
37
38
  rules: {
39
+ 'basalt/provider-above-router': 'error',
40
+ 'basalt/duplicate-notifications-mount': 'error',
41
+ 'basalt/query-dual-import': 'error',
42
+ 'basalt/query-fn-unwrap': 'error',
43
+ 'basalt/deprecated-export': 'error',
44
+ 'basalt/forms-field-key': 'error',
38
45
  'basalt/no-raw-font-size': 'error',
46
+ 'basalt/raw-size-literal': 'error',
39
47
  'basalt/card-inset': 'error',
40
48
  'basalt/chart-in-raw-surface': 'error',
41
49
  'basalt/hand-rolled-plot': 'error',
@@ -45,6 +53,7 @@ beforeEach(() => {
45
53
  'basalt/raw-scroll-container': 'error',
46
54
  'basalt/hand-rolled-filter': 'error',
47
55
  'basalt/control-outside-home': 'error',
56
+ 'basalt/bound-control-outside-home': 'error',
48
57
  'basalt/control-size-literal': 'error',
49
58
  'basalt/page-bar-budget': 'error',
50
59
  'basalt/in-body-page-title': 'error',
@@ -111,6 +120,76 @@ describe('basalt/no-raw-font-size', () => {
111
120
  expect(code).toBe(0)
112
121
  expect(rules).not.toContain('no-raw-font-size')
113
122
  })
123
+
124
+ // F25: symmetric with raw-size-literal's own test-file skip below — the doctrine is about
125
+ // shipped UI, not fixtures.
126
+ it('does NOT flag a numeric fz inside a *.test.tsx file', () => {
127
+ const { code, rules } = run(`export const C = () => <Text fz={10}>a</Text>\n`, 'lib.test.tsx')
128
+ expect(code).toBe(0)
129
+ expect(rules).not.toContain('no-raw-font-size')
130
+ })
131
+ })
132
+
133
+ // ── raw-size-literal ─────────────────────────────────────────────────────────
134
+
135
+ describe('basalt/raw-size-literal', () => {
136
+ it('flags a CSS-length string on size', () => {
137
+ const { code, rules } = run(`export const C = () => <ThemeIcon size="2rem" />\n`)
138
+ expect(code).toBe(1)
139
+ expect(rules).toContain('raw-size-literal')
140
+ })
141
+
142
+ it('flags a CSS-length string on fz', () => {
143
+ const { code, rules } = run(`export const C = () => <Text fz="0.8rem">a</Text>\n`)
144
+ expect(code).toBe(1)
145
+ expect(rules).toContain('raw-size-literal')
146
+ })
147
+
148
+ it('does NOT flag a scale token on size', () => {
149
+ const { code, rules } = run(`export const C = () => <ThemeIcon size="md" />\n`)
150
+ expect(code).toBe(0)
151
+ expect(rules).not.toContain('raw-size-literal')
152
+ })
153
+
154
+ it('does NOT flag a numeric size (the documented icon-dimension idiom)', () => {
155
+ const { code, rules } = run(`export const C = () => <ThemeIcon size={32} />\n`)
156
+ expect(code).toBe(0)
157
+ expect(rules).not.toContain('raw-size-literal')
158
+ })
159
+
160
+ it('does NOT flag with a same-line theme-allow comment', () => {
161
+ const { code, rules } = run(
162
+ `export const C = () => <Text fz="0.8rem" /* theme-allow: legacy */>a</Text>\n`,
163
+ )
164
+ expect(code).toBe(0)
165
+ expect(rules).not.toContain('raw-size-literal')
166
+ })
167
+
168
+ // F25: was the asymmetric half — no-raw-font-size already skipped test files, this rule did not.
169
+ it('does NOT flag a CSS-length string inside a *.test.tsx file', () => {
170
+ const { code, rules } = run(
171
+ `export const C = () => <Text fz="0.8rem">a</Text>\n`,
172
+ 'lib.test.tsx',
173
+ )
174
+ expect(code).toBe(0)
175
+ expect(rules).not.toContain('raw-size-literal')
176
+ })
177
+
178
+ // A3: the /src/tokens/ exemption used to be a bare path-substring check, then a check gated on
179
+ // `isBasaltScopedFile` — but that predicate tests "does the nearest package.json depend on
180
+ // basalt-ui", not "is this file basalt's own", so a CONSUMER package that merely depends on
181
+ // basalt-ui could still put raw literals under its own `src/tokens/` and silence the rule. The
182
+ // exemption is deleted outright rather than re-gated on package identity: both rules only ever
183
+ // visit JSX, and basalt's real `src/tokens/**` is pure `.ts` data with none, so no gate was ever
184
+ // load-bearing there. A `src/tokens/` path — basalt's own or a consumer's — now always fires.
185
+ it('DOES flag a CSS-length string inside a src/tokens/ file (no path exemption)', () => {
186
+ const { code, rules } = run(
187
+ `export const C = () => <ThemeIcon size="2rem" />\n`,
188
+ 'src/tokens/palette.tsx',
189
+ )
190
+ expect(code).toBe(1)
191
+ expect(rules).toContain('raw-size-literal')
192
+ })
114
193
  })
115
194
 
116
195
  // ── card-inset ───────────────────────────────────────────────────────────────
@@ -1296,6 +1375,30 @@ describe('PLUGIN_RULE_GRACE', () => {
1296
1375
  it('passes the C16 gate against the real ledger and package version', () => {
1297
1376
  expect(() => assertGraceLedger(Object.fromEntries(graceEntries), pkgVersion)).not.toThrow()
1298
1377
  })
1378
+
1379
+ // F27: a grace entry is written for a rule that lands in the NEXT release, so its `since` can sit
1380
+ // one minor ahead of `package.json`'s current version (`bound-control-outside-home` at 1.28.0
1381
+ // while the published version still reads 1.27.0, exactly like `control-outside-home` before it).
1382
+ // What this catches is a `since` that is malformed or planted further out than that — a value
1383
+ // static analysis has no way to verify is "the version this actually ships in", so the bound this
1384
+ // test enforces is the one thing that IS checkable: well-formed semver, no more than one minor
1385
+ // past what is currently published.
1386
+ it("every grace entry's since is valid semver, at most one minor past the published version", () => {
1387
+ const SEMVER = /^\d+\.\d+\.\d+$/
1388
+ const [major, minor] = pkgVersion.split('.').map(Number)
1389
+ const nextMinorCeiling = `${major}.${minor + 1}.0`
1390
+ for (const [id, entry] of graceEntries) {
1391
+ if (!SEMVER.test(entry.since)) {
1392
+ throw new Error(`${id}: \`since\` (${entry.since}) is not valid semver.`)
1393
+ }
1394
+ if (compareSemver(entry.since, nextMinorCeiling) > 0) {
1395
+ throw new Error(
1396
+ `${id}: \`since\` (${entry.since}) is more than one minor past the published version ` +
1397
+ `${pkgVersion}.`,
1398
+ )
1399
+ }
1400
+ }
1401
+ })
1299
1402
  })
1300
1403
 
1301
1404
  // ── chart-legend-literal: the .map() half ────────────────────────────────────
@@ -1681,6 +1784,28 @@ describe('basalt/control-outside-home', () => {
1681
1784
  },
1682
1785
  )
1683
1786
 
1787
+ // FormRow/FormGroup (`basalt-ui/forms`) are law C1's third home written for a `<form>` rather
1788
+ // than a settings page — the same non-home treatment SettingsRow gets, via the same
1789
+ // CONTROL_HOST_TAGS ancestry walk.
1790
+ it.each(['FormRow', 'FormGroup'])(
1791
+ 'does NOT flag a raw Select nested inside a %s imported from basalt-ui/forms',
1792
+ (host) => {
1793
+ const { code, rules } = run(
1794
+ `import { ${host} } from 'basalt-ui/forms'\n${MANTINE_IMPORT}` +
1795
+ `export const C = () => <${host} label="L"><Select data={[]} /></${host}>\n`,
1796
+ )
1797
+ expect(code).toBe(0)
1798
+ expect(rules).not.toContain('control-outside-home')
1799
+ },
1800
+ )
1801
+
1802
+ it('still flags a bare Select in a page body with no FormRow/FormGroup around it', () => {
1803
+ const { rules } = run(
1804
+ `${MANTINE_IMPORT}export const C = () => <div><Select data={[]} /></div>\n`,
1805
+ )
1806
+ expect(rules).toContain('control-outside-home')
1807
+ })
1808
+
1684
1809
  it('does NOT flag one under a member-expression non-home (Menu.Dropdown)', () => {
1685
1810
  const { rules } = run(
1686
1811
  `${MANTINE_IMPORT}export const C = () => <Menu.Dropdown><Select data={[]} /></Menu.Dropdown>\n`,
@@ -1708,6 +1833,58 @@ describe('basalt/control-outside-home', () => {
1708
1833
  expect(rules).not.toContain('control-outside-home')
1709
1834
  })
1710
1835
 
1836
+ /**
1837
+ * The exemption's SECOND half. `CONTROL_OWNER_NAMES` carries generic names (`PanelRow`,
1838
+ * `EnumFilter`, `SliderControl`), so a bare-name match let one local helper switch this rule —
1839
+ * and `bound-control-outside-home` and `responsive-twin` with it — off for a whole consumer file.
1840
+ * A file that imports `basalt-ui*` is CONSUMING basalt; basalt's own control sources import each
1841
+ * other relatively and never name the package.
1842
+ */
1843
+ it('DOES flag a file that declares its own PanelRow but imports basalt-ui', () => {
1844
+ const { rules } = run(
1845
+ `import { Section } from 'basalt-ui'\n${MANTINE_IMPORT}` +
1846
+ `function PanelRow({ children }) {\n return <div>{children}</div>\n}\n` +
1847
+ `export const C = () => (\n <Section title="x">\n <Select value={v} onChange={set} data={[]} />\n </Section>\n)\n`,
1848
+ )
1849
+ expect(rules).toContain('control-outside-home')
1850
+ })
1851
+
1852
+ /**
1853
+ * The basalt SUBTREE homes, which this rule did not know: `bound-control-outside-home` has read
1854
+ * `FilterSet` / `PageAside` / `PanelRow` as homes since it shipped, so a `<Select>` inside a
1855
+ * `<PageAside><PanelRow>` reported while the bound control beside it did not — two rules
1856
+ * answering one question two ways.
1857
+ */
1858
+ it('does NOT flag one inside a PageAside > PanelRow subtree', () => {
1859
+ const { code, rules } = run(
1860
+ `import { PageAside } from 'basalt-ui'\nimport { PanelRow } from 'basalt-ui/controls'\n${MANTINE_IMPORT}` +
1861
+ `export const C = () => (\n <PageAside title="Filters">\n <PanelRow label="Scale">\n <Select data={[]} />\n </PanelRow>\n </PageAside>\n)\n`,
1862
+ 'src/scale-page.tsx',
1863
+ )
1864
+ expect(code).toBe(0)
1865
+ expect(rules).not.toContain('control-outside-home')
1866
+ })
1867
+
1868
+ it('does NOT flag one inside a FilterSet subtree', () => {
1869
+ const { rules } = run(
1870
+ `import { FilterSet } from 'basalt-ui/controls'\n${MANTINE_IMPORT}` +
1871
+ `export const C = () => (\n <FilterSet>\n <Select data={[]} />\n </FilterSet>\n)\n`,
1872
+ 'src/scale-page.tsx',
1873
+ )
1874
+ expect(rules).not.toContain('control-outside-home')
1875
+ })
1876
+
1877
+ // Provenance, the test `SLOT_OWNER_TAGS` already applies: a consumer's own `PanelRow` is a layout
1878
+ // helper, not a basalt home, so it exempts nothing.
1879
+ it("still flags one inside a consumer's OWN PanelRow", () => {
1880
+ const { rules } = run(
1881
+ `import { PanelRow } from './panel-row'\n${MANTINE_IMPORT}` +
1882
+ `export const C = () => (\n <PanelRow label="Scale">\n <Select data={[]} />\n </PanelRow>\n)\n`,
1883
+ 'src/scale-page.tsx',
1884
+ )
1885
+ expect(rules).toContain('control-outside-home')
1886
+ })
1887
+
1711
1888
  it('flags an ALIASED Mantine import with no home', () => {
1712
1889
  const { code, rules } = run(
1713
1890
  `import { Select as MantineSelect } from '@mantine/core'\n` +
@@ -1731,6 +1908,292 @@ describe('basalt/control-outside-home', () => {
1731
1908
  )
1732
1909
  expect(rules).not.toContain('control-outside-home')
1733
1910
  })
1911
+
1912
+ /**
1913
+ * The CROSS-FILE case, exempted by a naming convention rather than by ancestry — argo carried 9 of
1914
+ * these, each a control in a modal/form module whose `<Modal>` is rendered by the parent route.
1915
+ * Nothing in the flagged file can see the host, so the ancestry walk never could have.
1916
+ */
1917
+ describe('the overlay filename convention', () => {
1918
+ const SOURCE = `${MANTINE_IMPORT}export const C = () => <div><Select data={[]} /></div>\n`
1919
+
1920
+ it.each([
1921
+ 'src/edit-session-modal.tsx',
1922
+ 'src/filters-drawer.tsx',
1923
+ 'src/column-popover.tsx',
1924
+ 'src/detail-panel.tsx',
1925
+ 'src/booking-form.tsx',
1926
+ ])('does NOT flag %s — its host lives in the parent', (filename) => {
1927
+ const { code, rules } = run(SOURCE, filename)
1928
+ expect(code).toBe(0)
1929
+ expect(rules).not.toContain('control-outside-home')
1930
+ })
1931
+
1932
+ it('still flags a plain page module beside them', () => {
1933
+ const { rules } = run(SOURCE, 'src/bookings-page.tsx')
1934
+ expect(rules).toContain('control-outside-home')
1935
+ })
1936
+
1937
+ // Basename only: a directory of modals holds the page pieces around them too, so a whole
1938
+ // `modal/` tree going silent is the version of this exemption that WOULD switch the rule off.
1939
+ it('is a BASENAME convention, not a directory one', () => {
1940
+ const { rules } = run(SOURCE, 'src/modal/session.tsx')
1941
+ expect(rules).toContain('control-outside-home')
1942
+ })
1943
+
1944
+ // `modal.tsx` with no leading segment is a page module in every consumer that has one; the
1945
+ // convention is `<subject>-modal.tsx`, and the regex says so.
1946
+ it('needs the leading subject — a bare modal.tsx is not the convention', () => {
1947
+ const { rules } = run(SOURCE, 'src/modal.tsx')
1948
+ expect(rules).toContain('control-outside-home')
1949
+ })
1950
+
1951
+ // The SECOND dialect: a repo mandating `PascalCase.tsx` for component files (basalt's own root
1952
+ // CLAUDE.md included) can never write `foo-panel.tsx`, so the kebab form alone exempted nothing
1953
+ // there — which is how the CBBI panel collected four warns for a `<PageAside>` in its parent.
1954
+ it.each(['src/EditSessionModal.tsx', 'src/FiltersDrawer.tsx', 'src/CbbiPanel.tsx'])(
1955
+ 'does NOT flag %s — the PascalCase dialect of the same convention',
1956
+ (filename) => {
1957
+ const { code, rules } = run(SOURCE, filename)
1958
+ expect(code).toBe(0)
1959
+ expect(rules).not.toContain('control-outside-home')
1960
+ },
1961
+ )
1962
+
1963
+ /**
1964
+ * The trade, stated as a test rather than only as prose: a `FooPanel.tsx` that renders NO page
1965
+ * bar is exempt WHOLE-FILE, so a control that genuinely belongs in a bar goes unreported there.
1966
+ * Measured before keeping it — across argo, linewatch, image-share, rb and image-gen the
1967
+ * PascalCase dialect matches 9 files, all in image-gen, and NONE of them renders a `<PageBar>`
1968
+ * or sits under a `routes/` directory. Nine overlay bodies, zero pages, so the dialect stays
1969
+ * whole-file rather than growing a "does it render a page bar" predicate (CONTROLS-SPEC §6).
1970
+ */
1971
+ it('exempts a FooPanel.tsx that renders no page bar — the measured trade', () => {
1972
+ const { code, rules } = run(SOURCE, 'src/components/refine/CropPanel.tsx')
1973
+ expect(code).toBe(0)
1974
+ expect(rules).not.toContain('control-outside-home')
1975
+ })
1976
+
1977
+ it('needs the leading subject in the PascalCase dialect too', () => {
1978
+ const { rules } = run(SOURCE, 'src/Panel.tsx')
1979
+ expect(rules).toContain('control-outside-home')
1980
+ })
1981
+
1982
+ it('is CASE-sensitive — foopanel.tsx is neither dialect', () => {
1983
+ const { rules } = run(SOURCE, 'src/foopanel.tsx')
1984
+ expect(rules).toContain('control-outside-home')
1985
+ })
1986
+ })
1987
+ })
1988
+
1989
+ /**
1990
+ * Ledger G5 (`docs/ASIDE-SPEC.md` §2) — the basalt half of law C1. Mirrors the
1991
+ * `control-outside-home` block above case for case, because the two rules answer the same question
1992
+ * about a different tag set: that one matches raw Mantine, this one matches a bound basalt control.
1993
+ */
1994
+ describe('basalt/bound-control-outside-home', () => {
1995
+ const CONTROLS_IMPORT = `import { FilterSet, PanelRow, SelectFilter, SliderControl } from 'basalt-ui/controls'\n`
1996
+ const ASIDE_IMPORT = `import { PageAside, PageBar, Section } from 'basalt-ui'\n`
1997
+
1998
+ it("flags a bound control in a Section's BODY — the stray pill G5 named", () => {
1999
+ const { code, rules } = run(
2000
+ `${ASIDE_IMPORT}${CONTROLS_IMPORT}export const C = () => (\n <Section title="Composition">\n <SelectFilter field={f} />\n </Section>\n)\n`,
2001
+ )
2002
+ expect(code).toBe(1)
2003
+ expect(rules).toContain('bound-control-outside-home')
2004
+ })
2005
+
2006
+ it('flags one in a bare page Stack', () => {
2007
+ const { rules } = run(
2008
+ `import { Stack } from '@mantine/core'\n${CONTROLS_IMPORT}` +
2009
+ `export const C = () => (\n <Stack>\n <SelectFilter field={f} />\n </Stack>\n)\n`,
2010
+ )
2011
+ expect(rules).toContain('bound-control-outside-home')
2012
+ })
2013
+
2014
+ it('does NOT flag one in a PageBar filters slot', () => {
2015
+ const { code, rules } = run(
2016
+ `${ASIDE_IMPORT}${CONTROLS_IMPORT}export const C = () => <PageBar filters={<SelectFilter field={f} />} />\n`,
2017
+ )
2018
+ expect(code).toBe(0)
2019
+ expect(rules).not.toContain('bound-control-outside-home')
2020
+ })
2021
+
2022
+ it('does NOT flag one inside a FilterSet subtree', () => {
2023
+ const { code, rules } = run(
2024
+ `${CONTROLS_IMPORT}export const C = () => (\n <FilterSet>\n <SelectFilter field={f} />\n </FilterSet>\n)\n`,
2025
+ )
2026
+ expect(code).toBe(0)
2027
+ expect(rules).not.toContain('bound-control-outside-home')
2028
+ })
2029
+
2030
+ // The aside is a shell REGION whose body IS a home — it scopes its children to the `panel`
2031
+ // surface, where the same control renders as a row rather than a pill (docs/ASIDE-SPEC.md §3).
2032
+ it('does NOT flag one inside a PageAside subtree, Section and all', () => {
2033
+ const { code, rules } = run(
2034
+ `${ASIDE_IMPORT}${CONTROLS_IMPORT}export const C = () => (\n <PageAside title="Filters">\n <Section title="Origin">\n <SelectFilter field={f} />\n </Section>\n </PageAside>\n)\n`,
2035
+ )
2036
+ expect(code).toBe(0)
2037
+ expect(rules).not.toContain('bound-control-outside-home')
2038
+ })
2039
+
2040
+ it("does NOT flag one as a PanelRow's `end` — the attribute hangs off PanelRow itself", () => {
2041
+ const { code, rules } = run(
2042
+ `${CONTROLS_IMPORT}export const C = () => <PanelRow label="Weight" end={<SelectFilter field={f} />} />\n`,
2043
+ )
2044
+ expect(code).toBe(0)
2045
+ expect(rules).not.toContain('bound-control-outside-home')
2046
+ })
2047
+
2048
+ it("does NOT flag one in a PanelRow's children — the row IS the home", () => {
2049
+ const { rules } = run(
2050
+ `${CONTROLS_IMPORT}export const C = () => (\n <PanelRow label="Origin">\n <SelectFilter field={f} />\n </PanelRow>\n)\n`,
2051
+ )
2052
+ expect(rules).not.toContain('bound-control-outside-home')
2053
+ })
2054
+
2055
+ it('does NOT flag one inside an overlay host (Modal)', () => {
2056
+ const { rules } = run(
2057
+ `import { Modal } from '@mantine/core'\n${CONTROLS_IMPORT}` +
2058
+ `export const C = () => (\n <Modal opened>\n <SelectFilter field={f} />\n </Modal>\n)\n`,
2059
+ )
2060
+ expect(rules).not.toContain('bound-control-outside-home')
2061
+ })
2062
+
2063
+ // FormRow/FormGroup are the same declared non-home as SettingsRow — shared CONTROL_HOST_TAGS walk.
2064
+ it.each(['FormRow', 'FormGroup'])('does NOT flag one inside a %s', (host) => {
2065
+ const { code, rules } = run(
2066
+ `import { ${host} } from 'basalt-ui/forms'\n${CONTROLS_IMPORT}` +
2067
+ `export const C = () => <${host} label="L"><SelectFilter field={f} /></${host}>\n`,
2068
+ )
2069
+ expect(code).toBe(0)
2070
+ expect(rules).not.toContain('bound-control-outside-home')
2071
+ })
2072
+
2073
+ it.each(['src/cbbi-panel.tsx', 'src/CbbiPanel.tsx'])(
2074
+ 'does NOT flag %s — a panel body whose host lives in the parent, in either dialect',
2075
+ (filename) => {
2076
+ const { code, rules } = run(
2077
+ `${CONTROLS_IMPORT}export const C = () => <div><SelectFilter field={f} /></div>\n`,
2078
+ filename,
2079
+ )
2080
+ expect(code).toBe(0)
2081
+ expect(rules).not.toContain('bound-control-outside-home')
2082
+ },
2083
+ )
2084
+
2085
+ // Both dialects need the leading SUBJECT, and neither is case-insensitive — `foopanel.tsx` is a
2086
+ // page module spelled without a separator, not a declaration.
2087
+ it.each(['src/foopanel.tsx', 'src/Panel.tsx'])('still flags %s', (filename) => {
2088
+ const { rules } = run(
2089
+ `${CONTROLS_IMPORT}export const C = () => <div><SelectFilter field={f} /></div>\n`,
2090
+ filename,
2091
+ )
2092
+ expect(rules).toContain('bound-control-outside-home')
2093
+ })
2094
+
2095
+ it('honors a scoped theme-allow', () => {
2096
+ const { rules } = run(
2097
+ `${CONTROLS_IMPORT}export const C = () => (\n <div>\n {/* theme-allow bound-control-outside-home — a one-off embedded picker */}\n <SelectFilter field={f} />\n </div>\n)\n`,
2098
+ )
2099
+ expect(rules).not.toContain('bound-control-outside-home')
2100
+ })
2101
+
2102
+ it('honors a theme-allow-file declaration', () => {
2103
+ const { rules } = run(
2104
+ `// theme-allow-file bound-control-outside-home — a legacy page, migrating next sprint\n` +
2105
+ `${CONTROLS_IMPORT}export const C = () => <div><SelectFilter field={f} /></div>\n`,
2106
+ )
2107
+ expect(rules).not.toContain('bound-control-outside-home')
2108
+ })
2109
+
2110
+ // Provenance, the same test the raw-filter rules apply the other way round: a consumer's own
2111
+ // `SelectFilter` is not basalt's, and basalt has no claim over where it is written.
2112
+ it('does NOT flag a locally-defined SelectFilter', () => {
2113
+ const { code, rules } = run(
2114
+ `import { SelectFilter } from './my-filters'\n` +
2115
+ `export const C = () => <div><SelectFilter field={f} /></div>\n`,
2116
+ )
2117
+ expect(code).toBe(0)
2118
+ expect(rules).not.toContain('bound-control-outside-home')
2119
+ })
2120
+
2121
+ it('flags an ALIASED basalt import with no home', () => {
2122
+ const { rules } = run(
2123
+ `import { SelectFilter as Picker } from 'basalt-ui/controls'\n` +
2124
+ `export const C = () => <div><Picker field={f} /></div>\n`,
2125
+ )
2126
+ expect(rules).toContain('bound-control-outside-home')
2127
+ })
2128
+
2129
+ /**
2130
+ * The owner exemption needs the file to import NOTHING from `basalt-ui*` — see
2131
+ * `control-outside-home`'s copy for why. For THIS rule that makes the exemption reachable only
2132
+ * inside basalt's own `src/`, which is exactly where it is meant to apply: provenance already
2133
+ * requires a basalt import (or a relative one inside that tree), so a consumer file can never be
2134
+ * both the owner and a place this rule could fire.
2135
+ */
2136
+ it('DOES flag a consumer file that declares its own PanelRow', () => {
2137
+ const { rules } = run(
2138
+ `${CONTROLS_IMPORT}function PanelRow({ children }) {\n return <div>{children}</div>\n}\n` +
2139
+ `export const C = () => <div><SelectFilter field={f} /></div>\n`,
2140
+ )
2141
+ expect(rules).toContain('bound-control-outside-home')
2142
+ })
2143
+
2144
+ // Provenance on the HOME too, the same test `control-outside-home` applies: a consumer's own
2145
+ // `PanelRow` is a layout helper, not the aside's row primitive.
2146
+ it("still flags one inside a consumer's OWN PanelRow", () => {
2147
+ const { rules } = run(
2148
+ `import { SelectFilter } from 'basalt-ui/controls'\nimport { PanelRow } from './panel-row'\n` +
2149
+ `export const C = () => (\n <PanelRow label="Scale">\n <SelectFilter field={f} />\n </PanelRow>\n)\n`,
2150
+ )
2151
+ expect(rules).toContain('bound-control-outside-home')
2152
+ })
2153
+
2154
+ /**
2155
+ * `SliderControl` is NOT policed: it renders its own `PanelRow` and has no pill form at all
2156
+ * (`docs/ASIDE-SPEC.md` §3), so "this renders as a stray pill" is false for it and a `Section`
2157
+ * body is a legitimate place to write one. The plugin has no per-tag home set, so the honest fix
2158
+ * is dropping it from the policed set rather than giving it a private exemption.
2159
+ */
2160
+ it('does NOT flag a SliderControl in a Section body — it has no pill form', () => {
2161
+ const { code, rules } = run(
2162
+ `${ASIDE_IMPORT}${CONTROLS_IMPORT}export const C = () => (\n <Section title="Weights">\n <SliderControl field={f} label="Pi" />\n </Section>\n)\n`,
2163
+ )
2164
+ expect(code).toBe(0)
2165
+ expect(rules).not.toContain('bound-control-outside-home')
2166
+ })
2167
+
2168
+ it('resolves a hoisted binding handed to a slot, like its siblings do', () => {
2169
+ const { rules } = run(
2170
+ `${ASIDE_IMPORT}${CONTROLS_IMPORT}const pills = <SelectFilter field={f} />\n` +
2171
+ `export const C = () => <PageBar filters={pills} />\n`,
2172
+ )
2173
+ expect(rules).not.toContain('bound-control-outside-home')
2174
+ })
2175
+
2176
+ /**
2177
+ * …and one handed to a subtree home as its CHILDREN, which the ancestry walk cannot see: at the
2178
+ * `const` there is no `PageAside` above the node at all. The same `Program:exit` deferral the
2179
+ * slot lane uses, over the home's `{expr}` children instead of its attributes.
2180
+ */
2181
+ it('resolves a hoisted binding rendered as a PageAside CHILD', () => {
2182
+ const { code, rules } = run(
2183
+ `${ASIDE_IMPORT}${CONTROLS_IMPORT}const rows = <SelectFilter field={f} />\n` +
2184
+ `export const C = () => <PageAside title="Filters">{rows}</PageAside>\n`,
2185
+ )
2186
+ expect(code).toBe(0)
2187
+ expect(rules).not.toContain('bound-control-outside-home')
2188
+ })
2189
+
2190
+ it('still flags the same hoisted binding under a bare Stack', () => {
2191
+ const { rules } = run(
2192
+ `import { Stack } from '@mantine/core'\n${CONTROLS_IMPORT}const rows = <SelectFilter field={f} />\n` +
2193
+ `export const C = () => <Stack>{rows}</Stack>\n`,
2194
+ )
2195
+ expect(rules).toContain('bound-control-outside-home')
2196
+ })
1734
2197
  })
1735
2198
 
1736
2199
  describe('basalt/control-size-literal', () => {
@@ -1761,6 +2224,82 @@ describe('basalt/control-size-literal', () => {
1761
2224
  expect(rules).not.toContain('control-size-literal')
1762
2225
  })
1763
2226
 
2227
+ /**
2228
+ * `ChartCard` is the one slot owner whose slot cannot mount the tier theme — it lives inside the
2229
+ * Mantine-free chart layer, so it writes `data-basalt-tier` by hand and the control there really
2230
+ * does have to state its own size. The rule fires on the `size` ATTRIBUTE and cannot tell a
2231
+ * correct `ctl` from the `xs` it exists to catch, so this is an exemption, not a waiver.
2232
+ */
2233
+ describe('ChartCard.actions — the one slot with no tier theme', () => {
2234
+ it('does NOT flag a size prop there', () => {
2235
+ const { code, rules } = run(
2236
+ `${BASALT_IMPORT}${MANTINE_IMPORT}export const C = () => <ChartCard title="T" actions={<Button size="ctl">Go</Button>} />
2237
+ `,
2238
+ )
2239
+ expect(code).toBe(0)
2240
+ expect(rules).not.toContain('control-size-literal')
2241
+ })
2242
+
2243
+ // The owner test reads the IMPORTED name, so the alias every wrapper writes still resolves.
2244
+ it('does NOT flag it under an aliased ChartCard import', () => {
2245
+ const { rules } = run(
2246
+ `import { ChartCard as Card } from 'basalt-ui'
2247
+ ${MANTINE_IMPORT}` +
2248
+ `export const C = () => <Card title="T" actions={<Button size="xs">Go</Button>} />
2249
+ `,
2250
+ )
2251
+ expect(rules).not.toContain('control-size-literal')
2252
+ })
2253
+
2254
+ // The positive control: every OTHER home does mount the theme, so the rule still fires there.
2255
+ it('STILL flags the same prop in a Section slot', () => {
2256
+ const { code, rules } = run(
2257
+ `${BASALT_IMPORT}${MANTINE_IMPORT}export const C = () => <Section title="T" actions={<Button size="xs">Go</Button>} />
2258
+ `,
2259
+ )
2260
+ expect(code).toBe(1)
2261
+ expect(rules).toContain('control-size-literal')
2262
+ })
2263
+
2264
+ // The exemption is keyed on the OWNER, and one hoisted binding can be handed to several — so it
2265
+ // holds only when EVERY home it reached is tierless. A last-writer-wins owner map made the
2266
+ // verdict depend on which attribute came later in the file: same code, opposite answer.
2267
+ it('STILL flags a hoisted binding shared with a Section slot — ChartCard last', () => {
2268
+ const { rules } = run(
2269
+ `${BASALT_IMPORT}${MANTINE_IMPORT}const acts = <Button size="xs">Go</Button>\n` +
2270
+ `export const C = () => (\n <>\n <Section title="S" actions={acts} />\n <ChartCard title="T" actions={acts} />\n </>\n)\n`,
2271
+ )
2272
+ expect(rules).toContain('control-size-literal')
2273
+ })
2274
+
2275
+ it('STILL flags a hoisted binding shared with a Section slot — ChartCard first', () => {
2276
+ const { rules } = run(
2277
+ `${BASALT_IMPORT}${MANTINE_IMPORT}const acts = <Button size="xs">Go</Button>\n` +
2278
+ `export const C = () => (\n <>\n <ChartCard title="T" actions={acts} />\n <Section title="S" actions={acts} />\n </>\n)\n`,
2279
+ )
2280
+ expect(rules).toContain('control-size-literal')
2281
+ })
2282
+
2283
+ it('does NOT flag a hoisted binding handed only to ChartCard', () => {
2284
+ const { code, rules } = run(
2285
+ `${BASALT_IMPORT}${MANTINE_IMPORT}const acts = <Button size="ctl">Go</Button>\n` +
2286
+ `export const C = () => <ChartCard title="T" actions={acts} />\n`,
2287
+ )
2288
+ expect(code).toBe(0)
2289
+ expect(rules).not.toContain('control-size-literal')
2290
+ })
2291
+
2292
+ // A consumer's OWN `ChartCard` is not basalt's, so it is not a home at all and nothing fires —
2293
+ // the same provenance rule every other home tag follows.
2294
+ it('hand-rolled-filter is NOT exempted there — the tier is not what makes a filter wrong', () => {
2295
+ const { rules } = run(
2296
+ `${BASALT_IMPORT}${MANTINE_IMPORT}export const C = () => <ChartCard title="T" actions={<Select data={[]} />} />
2297
+ `,
2298
+ )
2299
+ expect(rules).toContain('hand-rolled-filter')
2300
+ })
2301
+ })
2302
+
1764
2303
  it('does NOT flag a size prop on the HOME itself', () => {
1765
2304
  const { code, rules } = run(
1766
2305
  `${BASALT_IMPORT}${MANTINE_IMPORT}export const C = () => <PageBar w={200} actions={<Button>Go</Button>} />\n`,
@@ -2043,6 +2582,17 @@ describe('basalt/responsive-twin', () => {
2043
2582
  expect(rules).not.toContain('responsive-twin')
2044
2583
  })
2045
2584
 
2585
+ // The owner exemption's second half, the same predicate its two siblings apply: a consumer file
2586
+ // that names a helper `PanelRow` and imports basalt is consuming basalt, not defining a control.
2587
+ it('DOES flag a file that declares its own PanelRow but imports basalt-ui', () => {
2588
+ const { rules } = run(
2589
+ `import { Section } from 'basalt-ui'\n` +
2590
+ `function PanelRow({ children }) {\n return <div>{children}</div>\n}\n` +
2591
+ `export const C = () => (\n <div>\n <SegmentedControl visibleFrom="sm" data={[]} />\n <SegmentedControl hiddenFrom="sm" data={[]} />\n </div>\n)\n`,
2592
+ )
2593
+ expect(rules).toContain('responsive-twin')
2594
+ })
2595
+
2046
2596
  it('honors a scoped theme-allow', () => {
2047
2597
  const { rules } = run(
2048
2598
  `export const C = () => (\n <div>\n <SegmentedControl visibleFrom="sm" data={[]} />\n {/* theme-allow responsive-twin — two genuinely different data sets */}\n <SegmentedControl hiddenFrom="sm" data={[]} />\n </div>\n)\n`,
@@ -2232,42 +2782,801 @@ describe('basalt/shadow-basalt-export — SHADOW_ALIASES', () => {
2232
2782
  })
2233
2783
  })
2234
2784
 
2235
- // ── SURFACES ↔ the plugin registry ───────────────────────────────────────────
2785
+ /**
2786
+ * The provenance half of the alias table: a file that COMPOSES the export it renames is a wrapper,
2787
+ * not a fork. argo's three hero cards each imported `StatCard`, wrapped it, and each took a warn
2788
+ * telling them to import the thing they were already importing.
2789
+ *
2790
+ * Both directions are asserted, because the exemption is only worth having if the rule still fires
2791
+ * WITHOUT the import — a wrapper is evidence, and silence on every `HeroCard` in the repo would be
2792
+ * the rule switching itself off.
2793
+ */
2794
+ describe('basalt/shadow-basalt-export — an alias that composes what it renames', () => {
2795
+ it('fires on a HeroCard that does not import StatCard', () => {
2796
+ const { rules, output } = run(
2797
+ `export function HeroCard() {\n return null\n}\n`,
2798
+ 'src/hero-card.tsx',
2799
+ )
2800
+ expect(rules).toContain('shadow-basalt-export')
2801
+ expect(output).toContain('StatCard')
2802
+ })
2236
2803
 
2237
- describe('PLUGIN_RULE_ID_LIST ↔ the plugin', () => {
2238
- // surfaces.ts cannot import the plugin (it is dependency-free by contract, and the plugin is a
2239
- // standalone .js loaded out of a consumer's node_modules), so the list is hand-maintained and
2240
- // THIS is the gate that keeps it honest.
2241
- it('is exactly the plugin s registered rule ids', () => {
2242
- expect([...PLUGIN_RULE_ID_LIST].toSorted()).toEqual(Object.keys(basaltPlugin.rules).toSorted())
2804
+ it('is silent on a HeroCard that composes the imported StatCard', () => {
2805
+ const { code, rules } = run(
2806
+ `import { StatCard } from 'basalt-ui'\n` +
2807
+ `export function HeroCard() {\n return <StatCard title="x" value="1" />\n}\n`,
2808
+ 'src/hero-card.tsx',
2809
+ )
2810
+ expect(code).toBe(0)
2811
+ expect(rules).not.toContain('shadow-basalt-export')
2243
2812
  })
2244
2813
 
2245
- it('maps every plugin rule to exactly one surface', () => {
2246
- const owners = new Map<string, string[]>()
2247
- for (const [key, spec] of Object.entries(SURFACES)) {
2248
- if (spec.kind !== 'doctrine') continue
2249
- for (const id of spec.pluginRules) owners.set(id, [...(owners.get(id) ?? []), key])
2250
- }
2251
- for (const id of PLUGIN_RULE_ID_LIST) {
2252
- expect([id, owners.get(id)?.length ?? 0]).toEqual([id, 1])
2253
- }
2254
- // …and no surface claims an id the plugin does not register (the type already forbids it; this
2255
- // catches a list entry deleted without its surface).
2256
- for (const id of owners.keys()) {
2257
- expect([id, (PLUGIN_RULE_ID_LIST as readonly string[]).includes(id)]).toEqual([id, true])
2258
- }
2814
+ // The provenance test reads the IMPORTED name, not the local one — `StatCard as BaseStatCard` is
2815
+ // the canonical way a wrapper avoids shadowing its own import, and a local-name set would answer
2816
+ // `BaseStatCard` and miss it.
2817
+ it('is silent when the composed export is imported under an alias', () => {
2818
+ const { rules } = run(
2819
+ `import { StatCard as BaseStatCard } from 'basalt-ui'\n` +
2820
+ `export function HeroCard() {\n return <BaseStatCard title="x" value="1" />\n}\n`,
2821
+ 'src/hero-card.tsx',
2822
+ )
2823
+ expect(rules).not.toContain('shadow-basalt-export')
2259
2824
  })
2260
2825
 
2261
- it("puts the shell rule on the root barrel's surface (wave 6)", () => {
2262
- expect(SURFACES['.'].pluginRules).toContain('hand-rolled-shell')
2826
+ it('is silent from a basalt SUBPATH too, not just the root barrel', () => {
2827
+ const { rules } = run(
2828
+ `import { RangeFilter } from 'basalt-ui/controls'\n` +
2829
+ `export function WindowSelector() {\n return <RangeFilter field={{}} />\n}\n`,
2830
+ 'src/window-selector.tsx',
2831
+ )
2832
+ expect(rules).not.toContain('shadow-basalt-export')
2263
2833
  })
2264
2834
 
2265
- it('gives ./controls the control-tier rules', () => {
2266
- expect([...SURFACES['./controls'].pluginRules].toSorted()).toEqual([
2267
- 'control-outside-home',
2268
- 'control-size-literal',
2269
- 'hand-rolled-filter',
2270
- 'responsive-twin',
2835
+ // Importing SOMETHING from basalt is not the exemption — importing the thing you renamed is.
2836
+ it('still fires when the file imports a DIFFERENT basalt export', () => {
2837
+ const { rules } = run(
2838
+ `import { Section } from 'basalt-ui'\n` +
2839
+ `export function HeroCard() {\n return <Section title="x" />\n}\n`,
2840
+ 'src/hero-card.tsx',
2841
+ )
2842
+ expect(rules).toContain('shadow-basalt-export')
2843
+ })
2844
+
2845
+ // The COLLISION half is deliberately not exempted: a local `StatCard` beside an
2846
+ // `import { StatCard as Base }` kept the name AND a piece of the original, which is the fork
2847
+ // shape this rule most wants to see.
2848
+ it('still fires on a name COLLISION even when the export is imported under an alias', () => {
2849
+ const { rules } = run(
2850
+ `import { StatCard as Base } from 'basalt-ui'\n` +
2851
+ `export function StatCard() {\n return <Base title="x" value="1" />\n}\n`,
2852
+ 'src/stat-card.tsx',
2853
+ )
2854
+ expect(rules).toContain('shadow-basalt-export')
2855
+ })
2856
+
2857
+ // COMPOSITION, not import. A type-only import is erased at compile time, so the shape a
2858
+ // props-copying fork actually writes — `ComponentProps<typeof StatCard>` off an
2859
+ // `import type` — composes nothing and used to exempt the whole file.
2860
+ it('still fires on a type-only import of the export it renames', () => {
2861
+ const { rules } = run(
2862
+ `import type { StatCard } from 'basalt-ui'\n` +
2863
+ `export function HeroCard() {\n return <div />\n}\n`,
2864
+ 'src/hero-card.tsx',
2865
+ )
2866
+ expect(rules).toContain('shadow-basalt-export')
2867
+ })
2868
+
2869
+ it('still fires on an inline `type` specifier', () => {
2870
+ const { rules } = run(
2871
+ `import { type StatCard } from 'basalt-ui'\n` +
2872
+ `export function HeroCard() {\n return <div />\n}\n`,
2873
+ 'src/hero-card.tsx',
2874
+ )
2875
+ expect(rules).toContain('shadow-basalt-export')
2876
+ })
2877
+
2878
+ // `typeof StatCard` is a `TSTypeQuery` around a plain Identifier, so a VALUE import used only in
2879
+ // a type position has to be rejected too — otherwise the props-copying fork just drops `type`.
2880
+ it('still fires when a value import is only used in a type position', () => {
2881
+ const { rules } = run(
2882
+ `import { StatCard } from 'basalt-ui'\n` +
2883
+ `type P = { card: typeof StatCard }\n` +
2884
+ `export function HeroCard(props: P) {\n return <div>{String(props)}</div>\n}\n`,
2885
+ 'src/hero-card.tsx',
2886
+ )
2887
+ expect(rules).toContain('shadow-basalt-export')
2888
+ })
2889
+
2890
+ it('still fires on a dead import left behind after the body was re-rolled', () => {
2891
+ const { rules } = run(
2892
+ `import { StatCard } from 'basalt-ui'\n` +
2893
+ `export function HeroCard() {\n return <div />\n}\n`,
2894
+ 'src/hero-card.tsx',
2895
+ )
2896
+ expect(rules).toContain('shadow-basalt-export')
2897
+ })
2898
+
2899
+ // A JSX tag is not the only way to compose: a wrapper that hands the component on as a prop or to
2900
+ // `createElement` composes it just as much, and a JSX-only test would report both.
2901
+ it('is silent when the import is passed on as a prop rather than rendered', () => {
2902
+ const { code, rules } = run(
2903
+ `import { StatCard } from 'basalt-ui'\n` +
2904
+ `export function HeroCard() {\n return <Slot component={StatCard} />\n}\n`,
2905
+ 'src/hero-card.tsx',
2906
+ )
2907
+ expect(code).toBe(0)
2908
+ expect(rules).not.toContain('shadow-basalt-export')
2909
+ })
2910
+ })
2911
+
2912
+ // ── SURFACES ↔ the plugin registry ───────────────────────────────────────────
2913
+
2914
+ describe('PLUGIN_RULE_ID_LIST ↔ the plugin', () => {
2915
+ // surfaces.ts cannot import the plugin (it is dependency-free by contract, and the plugin is a
2916
+ // standalone .js loaded out of a consumer's node_modules), so the list is hand-maintained and
2917
+ // THIS is the gate that keeps it honest.
2918
+ it('is exactly the plugin s registered rule ids', () => {
2919
+ expect([...PLUGIN_RULE_ID_LIST].toSorted()).toEqual(Object.keys(basaltPlugin.rules).toSorted())
2920
+ })
2921
+
2922
+ it('maps every plugin rule to exactly one surface', () => {
2923
+ const owners = new Map<string, string[]>()
2924
+ for (const [key, spec] of Object.entries(SURFACES)) {
2925
+ if (spec.kind !== 'doctrine') continue
2926
+ for (const id of spec.pluginRules) owners.set(id, [...(owners.get(id) ?? []), key])
2927
+ }
2928
+ for (const id of PLUGIN_RULE_ID_LIST) {
2929
+ expect([id, owners.get(id)?.length ?? 0]).toEqual([id, 1])
2930
+ }
2931
+ // …and no surface claims an id the plugin does not register (the type already forbids it; this
2932
+ // catches a list entry deleted without its surface).
2933
+ for (const id of owners.keys()) {
2934
+ expect([id, (PLUGIN_RULE_ID_LIST as readonly string[]).includes(id)]).toEqual([id, true])
2935
+ }
2936
+ })
2937
+
2938
+ it("puts the shell rule on the root barrel's surface (wave 6)", () => {
2939
+ expect(SURFACES['.'].pluginRules).toContain('hand-rolled-shell')
2940
+ })
2941
+
2942
+ it('gives ./controls the control-tier rules', () => {
2943
+ expect([...SURFACES['./controls'].pluginRules].toSorted()).toEqual([
2944
+ 'bound-control-outside-home',
2945
+ 'control-outside-home',
2946
+ 'control-size-literal',
2947
+ 'hand-rolled-filter',
2948
+ 'responsive-twin',
2271
2949
  ])
2272
2950
  })
2273
2951
  })
2952
+
2953
+ // ── provider-above-router (F5) ───────────────────────────────────────────────
2954
+
2955
+ describe('basalt/provider-above-router', () => {
2956
+ const IMPORTS =
2957
+ `import { BasaltProvider } from 'basalt-ui'\n` +
2958
+ `import { RouterProvider } from '@tanstack/react-router'\n`
2959
+
2960
+ it('flags a BasaltProvider rendered inside a RouterProvider', () => {
2961
+ const { code, rules } = run(
2962
+ `${IMPORTS}export const App = () => (\n` +
2963
+ ` <RouterProvider router={r}>\n <BasaltProvider>{null}</BasaltProvider>\n` +
2964
+ ` </RouterProvider>\n)\n`,
2965
+ )
2966
+ expect(code).toBe(1)
2967
+ expect(rules).toContain('provider-above-router')
2968
+ })
2969
+
2970
+ it('does NOT flag the correct order — provider above router', () => {
2971
+ const { code, rules } = run(
2972
+ `${IMPORTS}export const App = () => (\n` +
2973
+ ` <BasaltProvider>\n <RouterProvider router={r} />\n </BasaltProvider>\n)\n`,
2974
+ )
2975
+ expect(code).toBe(0)
2976
+ expect(rules).not.toContain('provider-above-router')
2977
+ })
2978
+
2979
+ // Provenance both ways: the tag names alone are not the finding.
2980
+ it("does NOT flag a consumer's own BasaltProvider (no basalt import)", () => {
2981
+ const { code, rules } = run(
2982
+ `import { RouterProvider } from '@tanstack/react-router'\n` +
2983
+ `import { BasaltProvider } from './my-provider'\n` +
2984
+ `export const App = () => (\n <RouterProvider router={r}>\n` +
2985
+ ` <BasaltProvider>{null}</BasaltProvider>\n </RouterProvider>\n)\n`,
2986
+ )
2987
+ expect(code).toBe(0)
2988
+ expect(rules).not.toContain('provider-above-router')
2989
+ })
2990
+
2991
+ it('does NOT flag a RouterProvider that is not TanStack s', () => {
2992
+ const { code, rules } = run(
2993
+ `import { BasaltProvider } from 'basalt-ui'\n` +
2994
+ `import { RouterProvider } from 'other-router'\n` +
2995
+ `export const App = () => (\n <RouterProvider router={r}>\n` +
2996
+ ` <BasaltProvider>{null}</BasaltProvider>\n </RouterProvider>\n)\n`,
2997
+ )
2998
+ expect(code).toBe(0)
2999
+ expect(rules).not.toContain('provider-above-router')
3000
+ })
3001
+
3002
+ // The alias is why the ancestry is read by BINDING and not by written tag name.
3003
+ it('flags through an aliased RouterProvider import', () => {
3004
+ const { code, rules } = run(
3005
+ `import { BasaltProvider } from 'basalt-ui'\n` +
3006
+ `import { RouterProvider as Router } from '@tanstack/react-router'\n` +
3007
+ `export const App = () => (\n <Router router={r}>\n` +
3008
+ ` <BasaltProvider>{null}</BasaltProvider>\n </Router>\n)\n`,
3009
+ )
3010
+ expect(code).toBe(1)
3011
+ expect(rules).toContain('provider-above-router')
3012
+ })
3013
+
3014
+ it('honours a theme-allow naming the rule', () => {
3015
+ const { code, rules } = run(
3016
+ `${IMPORTS}export const App = () => (\n <RouterProvider router={r}>\n` +
3017
+ ` {/* theme-allow provider-above-router — legacy bootstrap, ported next minor */}\n` +
3018
+ ` <BasaltProvider>{null}</BasaltProvider>\n </RouterProvider>\n)\n`,
3019
+ )
3020
+ expect(code).toBe(0)
3021
+ expect(rules).not.toContain('provider-above-router')
3022
+ })
3023
+
3024
+ // The documented limit of the heuristic — stated in the rule's own JSDoc, pinned here so a
3025
+ // future widening has to move this test rather than discover the gap.
3026
+ it('does NOT see a cross-file composition (static JSX ancestry only)', () => {
3027
+ const { code, rules } = run(
3028
+ `import { BasaltProvider } from 'basalt-ui'\n` +
3029
+ `import { Routes } from './routes'\n` +
3030
+ `export const App = () => (\n <Routes>\n <BasaltProvider>{null}</BasaltProvider>\n` +
3031
+ ` </Routes>\n)\n`,
3032
+ )
3033
+ expect(code).toBe(0)
3034
+ expect(rules).not.toContain('provider-above-router')
3035
+ })
3036
+ })
3037
+
3038
+ // ── duplicate-notifications-mount (F5) ───────────────────────────────────────
3039
+
3040
+ describe('basalt/duplicate-notifications-mount', () => {
3041
+ const IMPORTS =
3042
+ `import { BasaltOverlays } from 'basalt-ui/commands'\n` +
3043
+ `import { BasaltNotifications } from 'basalt-ui/notifications'\n`
3044
+
3045
+ it('flags both mounts in one file', () => {
3046
+ const { code, rules } = run(
3047
+ `${IMPORTS}export const App = () => (\n <BasaltOverlays notifications>\n` +
3048
+ ` <BasaltNotifications />\n </BasaltOverlays>\n)\n`,
3049
+ )
3050
+ expect(code).toBe(1)
3051
+ expect(rules).toContain('duplicate-notifications-mount')
3052
+ })
3053
+
3054
+ // `notifications` defaults to TRUE, so the attribute being absent is the same defect.
3055
+ it('flags when BasaltOverlays writes no notifications prop at all', () => {
3056
+ const { code, rules } = run(
3057
+ `${IMPORTS}export const App = () => (\n <BasaltOverlays>\n` +
3058
+ ` <BasaltNotifications />\n </BasaltOverlays>\n)\n`,
3059
+ )
3060
+ expect(code).toBe(1)
3061
+ expect(rules).toContain('duplicate-notifications-mount')
3062
+ })
3063
+
3064
+ it('does NOT flag when BasaltOverlays disables its notifications layer', () => {
3065
+ const { code, rules } = run(
3066
+ `${IMPORTS}export const App = () => (\n <BasaltOverlays notifications={false}>\n` +
3067
+ ` <BasaltNotifications />\n </BasaltOverlays>\n)\n`,
3068
+ )
3069
+ expect(code).toBe(0)
3070
+ expect(rules).not.toContain('duplicate-notifications-mount')
3071
+ })
3072
+
3073
+ it('does NOT flag a standalone BasaltNotifications on its own', () => {
3074
+ const { code, rules } = run(
3075
+ `import { BasaltNotifications } from 'basalt-ui/notifications'\n` +
3076
+ `export const App = () => <BasaltNotifications />\n`,
3077
+ )
3078
+ expect(code).toBe(0)
3079
+ expect(rules).not.toContain('duplicate-notifications-mount')
3080
+ })
3081
+
3082
+ it("does NOT flag a consumer's own components of the same names", () => {
3083
+ const { code, rules } = run(
3084
+ `import { BasaltOverlays } from './overlays'\n` +
3085
+ `import { BasaltNotifications } from './notifications'\n` +
3086
+ `export const App = () => (\n <BasaltOverlays>\n <BasaltNotifications />\n` +
3087
+ ` </BasaltOverlays>\n)\n`,
3088
+ )
3089
+ expect(code).toBe(0)
3090
+ expect(rules).not.toContain('duplicate-notifications-mount')
3091
+ })
3092
+
3093
+ it('honours a theme-allow naming the rule', () => {
3094
+ const { code, rules } = run(
3095
+ `${IMPORTS}export const App = () => (\n <BasaltOverlays notifications>\n` +
3096
+ ` {/* theme-allow duplicate-notifications-mount — two roots, never both mounted */}\n` +
3097
+ ` <BasaltNotifications />\n </BasaltOverlays>\n)\n`,
3098
+ )
3099
+ expect(code).toBe(0)
3100
+ expect(rules).not.toContain('duplicate-notifications-mount')
3101
+ })
3102
+
3103
+ // Two mounts WRITTEN, one mount RENDERED — the ternary is the shape a consumer reaches for while
3104
+ // migrating from the standalone layer to the composed one, and it was never a double mount.
3105
+ it('does NOT flag the two branches of one ternary', () => {
3106
+ const { code, rules } = run(
3107
+ `${IMPORTS}export const App = ({ composed }) => (\n` +
3108
+ ` <div>{composed ? <BasaltOverlays /> : <BasaltNotifications />}</div>\n)\n`,
3109
+ )
3110
+ expect(code).toBe(0)
3111
+ expect(rules).not.toContain('duplicate-notifications-mount')
3112
+ })
3113
+
3114
+ it('does NOT flag the two operands of one logical expression', () => {
3115
+ const { code, rules } = run(
3116
+ `${IMPORTS}export const App = ({ composed }) => (\n` +
3117
+ ` <div>{(composed && <BasaltOverlays />) || <BasaltNotifications />}</div>\n)\n`,
3118
+ )
3119
+ expect(code).toBe(0)
3120
+ expect(rules).not.toContain('duplicate-notifications-mount')
3121
+ })
3122
+
3123
+ // The exemption is the NEAREST common ancestor only. Two separate `&&` guards can both hold, so
3124
+ // this stays a finding — the pair is conditional, not exclusive.
3125
+ it('DOES flag two independently-guarded mounts under one parent', () => {
3126
+ const { code, rules } = run(
3127
+ `${IMPORTS}export const App = ({ a, b }) => (\n` +
3128
+ ` <div>{a && <BasaltOverlays />}{b && <BasaltNotifications />}</div>\n)\n`,
3129
+ )
3130
+ expect(code).toBe(1)
3131
+ expect(rules).toContain('duplicate-notifications-mount')
3132
+ })
3133
+
3134
+ // One exclusive pair does not excuse a second, unconditional overlays mount.
3135
+ it('DOES flag when a NON-exclusive overlays mount also exists', () => {
3136
+ const { code, rules } = run(
3137
+ `${IMPORTS}export const App = ({ composed }) => (\n` +
3138
+ ` <BasaltOverlays>\n` +
3139
+ ` {composed ? <BasaltOverlays /> : <BasaltNotifications />}\n` +
3140
+ ` </BasaltOverlays>\n)\n`,
3141
+ )
3142
+ expect(code).toBe(1)
3143
+ expect(rules).toContain('duplicate-notifications-mount')
3144
+ })
3145
+ })
3146
+
3147
+ // ── query-dual-import (F5) ───────────────────────────────────────────────────
3148
+
3149
+ describe('basalt/query-dual-import', () => {
3150
+ it('flags a file importing from both @tanstack/react-query and basalt-ui/query', () => {
3151
+ const { code, rules, output } = run(
3152
+ `import { useQuery } from '@tanstack/react-query'\n` +
3153
+ `import { unwrap } from 'basalt-ui/query'\n` +
3154
+ `export const useThing = () => useQuery({ queryKey: ['t'], queryFn: () => unwrap(get()) })\n`,
3155
+ )
3156
+ expect(code).toBe(1)
3157
+ expect(rules).toContain('query-dual-import')
3158
+ expect(output).toContain('Dual query import')
3159
+ })
3160
+
3161
+ // The softer half — the one that actually catches the drift, and why the rule is `warn`.
3162
+ it('flags a raw @tanstack/react-query import beside a basalt root import', () => {
3163
+ const { code, rules, output } = run(
3164
+ `import { useQuery } from '@tanstack/react-query'\n` +
3165
+ `import { QueryState } from 'basalt-ui'\n` +
3166
+ `export const useThing = () => useQuery({ queryKey: ['t'], queryFn: get })\n`,
3167
+ )
3168
+ expect(code).toBe(1)
3169
+ expect(rules).toContain('query-dual-import')
3170
+ expect(output).toContain('Raw @tanstack/react-query import')
3171
+ })
3172
+
3173
+ it('does NOT flag basalt-ui/query on its own', () => {
3174
+ const { code, rules } = run(
3175
+ `import { useQuery, unwrap } from 'basalt-ui/query'\n` +
3176
+ `export const useThing = () => useQuery({ queryKey: ['t'], queryFn: () => unwrap(get()) })\n`,
3177
+ )
3178
+ expect(code).toBe(0)
3179
+ expect(rules).not.toContain('query-dual-import')
3180
+ })
3181
+
3182
+ // `import type` is erased at compile time — there is no runtime import to route through the
3183
+ // seam, and typing against the library's own result type is what `QueryStateLike` documents as
3184
+ // legitimate. Same skip `agent-no-raw-usechat` and `ai-sdk-major` take.
3185
+ it('does NOT flag a type-only @tanstack/react-query import beside basalt-ui/query', () => {
3186
+ const { code, rules } = run(
3187
+ `import type { UseQueryResult } from '@tanstack/react-query'\n` +
3188
+ `import { unwrap } from 'basalt-ui/query'\n` +
3189
+ `export const pick = (q: UseQueryResult<number>) => unwrap(q.data)\n`,
3190
+ )
3191
+ expect(code).toBe(0)
3192
+ expect(rules).not.toContain('query-dual-import')
3193
+ })
3194
+
3195
+ it('does NOT flag @tanstack/react-query in a file with no basalt import', () => {
3196
+ const { code, rules } = run(
3197
+ `import { useQuery } from '@tanstack/react-query'\n` +
3198
+ `export const useThing = () => useQuery({ queryKey: ['t'], queryFn: get })\n`,
3199
+ )
3200
+ expect(code).toBe(0)
3201
+ expect(rules).not.toContain('query-dual-import')
3202
+ })
3203
+
3204
+ // The devtools package is a different package, not a subpath.
3205
+ it('does NOT flag @tanstack/react-query-devtools', () => {
3206
+ const { code, rules } = run(
3207
+ `import { ReactQueryDevtools } from '@tanstack/react-query-devtools'\n` +
3208
+ `import { unwrap } from 'basalt-ui/query'\n` +
3209
+ `export const D = () => <ReactQueryDevtools />\n`,
3210
+ )
3211
+ expect(code).toBe(0)
3212
+ expect(rules).not.toContain('query-dual-import')
3213
+ })
3214
+
3215
+ it('honours a theme-allow naming the rule', () => {
3216
+ const { code, rules } = run(
3217
+ `// theme-allow query-dual-import — the persister API has no basalt re-export yet\n` +
3218
+ `import { useQuery } from '@tanstack/react-query'\n` +
3219
+ `import { unwrap } from 'basalt-ui/query'\n` +
3220
+ `export const useThing = () => useQuery({ queryKey: ['t'], queryFn: () => unwrap(g()) })\n`,
3221
+ )
3222
+ expect(code).toBe(0)
3223
+ expect(rules).not.toContain('query-dual-import')
3224
+ })
3225
+ })
3226
+
3227
+ // ── query-fn-unwrap (F5) ─────────────────────────────────────────────────────
3228
+
3229
+ describe('basalt/query-fn-unwrap', () => {
3230
+ const IMPORT = `import { unwrap } from 'basalt-ui/query'\n`
3231
+
3232
+ it('flags a queryFn that fetches with no unwrap', () => {
3233
+ const { code, rules } = run(
3234
+ `${IMPORT}export const o = { queryKey: ['t'], queryFn: () => fetch('/api/t') }\n`,
3235
+ )
3236
+ expect(code).toBe(1)
3237
+ expect(rules).toContain('query-fn-unwrap')
3238
+ })
3239
+
3240
+ it('flags the async .json() form', () => {
3241
+ const { code, rules } = run(
3242
+ `${IMPORT}export const o = {\n queryKey: ['t'],\n` +
3243
+ ` queryFn: async () => (await fetch('/api/t')).json(),\n}\n`,
3244
+ )
3245
+ expect(code).toBe(1)
3246
+ expect(rules).toContain('query-fn-unwrap')
3247
+ })
3248
+
3249
+ it('does NOT flag a queryFn that wraps the call', () => {
3250
+ const { code, rules } = run(
3251
+ `${IMPORT}export const o = { queryKey: ['t'], queryFn: () => unwrap(fetch('/api/t')) }\n`,
3252
+ )
3253
+ expect(code).toBe(0)
3254
+ expect(rules).not.toContain('query-fn-unwrap')
3255
+ })
3256
+
3257
+ it('does NOT flag a queryFn with no fetch in it', () => {
3258
+ const { code, rules } = run(
3259
+ `${IMPORT}export const o = { queryKey: ['t'], queryFn: () => client.t.get() }\n`,
3260
+ )
3261
+ expect(code).toBe(0)
3262
+ expect(rules).not.toContain('query-fn-unwrap')
3263
+ })
3264
+
3265
+ // The scope gate: with no basalt-ui/query import this is an opinion about `fetch`, which basalt
3266
+ // does not have.
3267
+ it('does NOT flag a file that never imports basalt-ui/query', () => {
3268
+ const { code, rules } = run(
3269
+ `export const o = { queryKey: ['t'], queryFn: () => fetch('/api/t') }\n`,
3270
+ )
3271
+ expect(code).toBe(0)
3272
+ expect(rules).not.toContain('query-fn-unwrap')
3273
+ })
3274
+
3275
+ // The documented blind spot — a reference, not a function literal. Pinned so a widening moves it.
3276
+ it('does NOT see a queryFn passed as a reference', () => {
3277
+ const { code, rules } = run(
3278
+ `${IMPORT}const load = () => fetch('/api/t')\n` +
3279
+ `export const o = { queryKey: ['t'], queryFn: load }\n`,
3280
+ )
3281
+ expect(code).toBe(0)
3282
+ expect(rules).not.toContain('query-fn-unwrap')
3283
+ })
3284
+
3285
+ it('honours a theme-allow naming the rule', () => {
3286
+ const { code, rules } = run(
3287
+ `${IMPORT}export const o = {\n queryKey: ['t'],\n` +
3288
+ ` // theme-allow query-fn-unwrap — raw Response is the point, this reads a blob\n` +
3289
+ ` queryFn: () => fetch('/api/t'),\n}\n`,
3290
+ )
3291
+ expect(code).toBe(0)
3292
+ expect(rules).not.toContain('query-fn-unwrap')
3293
+ })
3294
+ })
3295
+
3296
+ // ── deprecated-export (B4) ───────────────────────────────────────────────────
3297
+
3298
+ describe('basalt/deprecated-export', () => {
3299
+ it('flags a deprecated named import', () => {
3300
+ const { code, rules, output } = run(
3301
+ `import { field } from 'basalt-ui/forms'\nexport const f = field\n`,
3302
+ )
3303
+ expect(code).toBe(1)
3304
+ expect(rules).toContain('deprecated-export')
3305
+ expect(output).toContain('inputProps')
3306
+ })
3307
+
3308
+ it('does NOT flag the replacement', () => {
3309
+ const { code, rules } = run(
3310
+ `import { inputProps } from 'basalt-ui/forms'\nexport const f = inputProps\n`,
3311
+ )
3312
+ expect(code).toBe(0)
3313
+ expect(rules).not.toContain('deprecated-export')
3314
+ })
3315
+
3316
+ it('does NOT flag a same-named import from somewhere else', () => {
3317
+ const { code, rules } = run(`import { field } from 'basalt-ui/state'\nexport const f = field\n`)
3318
+ expect(code).toBe(0)
3319
+ expect(rules).not.toContain('deprecated-export')
3320
+ })
3321
+
3322
+ it('flags each deprecated BasaltProvider connectivity prop', () => {
3323
+ const { code, rules, output } = run(
3324
+ `import { BasaltProvider } from 'basalt-ui'\n` +
3325
+ `export const App = () => (\n` +
3326
+ ` <BasaltProvider sseUrl="/e" healthUrl="/h" healthIntervalMs={5000}>{null}</BasaltProvider>\n)\n`,
3327
+ )
3328
+ expect(code).toBe(1)
3329
+ expect(rules).toContain('deprecated-export')
3330
+ expect(output).toContain('connectivity')
3331
+ })
3332
+
3333
+ it('does NOT flag the connectivity object prop', () => {
3334
+ const { code, rules } = run(
3335
+ `import { BasaltProvider } from 'basalt-ui'\n` +
3336
+ `export const App = () => <BasaltProvider connectivity={{ sseUrl: '/e' }}>{null}</BasaltProvider>\n`,
3337
+ )
3338
+ expect(code).toBe(0)
3339
+ expect(rules).not.toContain('deprecated-export')
3340
+ })
3341
+
3342
+ it("does NOT flag an sseUrl on a consumer's own component", () => {
3343
+ const { code, rules } = run(
3344
+ `import { BasaltProvider } from './provider'\n` +
3345
+ `export const App = () => <BasaltProvider sseUrl="/e">{null}</BasaltProvider>\n`,
3346
+ )
3347
+ expect(code).toBe(0)
3348
+ expect(rules).not.toContain('deprecated-export')
3349
+ })
3350
+
3351
+ it('honours a theme-allow naming the rule', () => {
3352
+ const { code, rules } = run(
3353
+ `// theme-allow deprecated-export — pinned to 1.27.0 until the forms port lands\n` +
3354
+ `import { field } from 'basalt-ui/forms'\nexport const f = field\n`,
3355
+ )
3356
+ expect(code).toBe(0)
3357
+ expect(rules).not.toContain('deprecated-export')
3358
+ })
3359
+
3360
+ // `field` → `inputProps` is NOT a drop-in rename any more (`inputProps` returns no `key`), so
3361
+ // the row carries `fix: false`: the diagnostic still fires, but `--fix` must leave the import
3362
+ // alone — the rewrite belongs to `basalt/forms-field-key`, which also inserts the key.
3363
+ it('leaves a row with `fix: false` untouched under --fix while still reporting it', () => {
3364
+ const source = `import { field } from 'basalt-ui/forms'\nexport const f = field\n`
3365
+ writeFileSync(resolve(dir, 'fixture.tsx'), source)
3366
+ Bun.spawnSync([OXLINT_BIN, '-c', '.oxlintrc.json', '--fix', 'fixture.tsx'], { cwd: dir })
3367
+ expect(readFileSync(resolve(dir, 'fixture.tsx'), 'utf8')).toBe(source)
3368
+ const { rules } = run(source)
3369
+ expect(rules).toContain('deprecated-export')
3370
+ })
3371
+ })
3372
+
3373
+ // ── forms-field-key ──────────────────────────────────────────────────────────
3374
+
3375
+ describe('basalt/forms-field-key', () => {
3376
+ const FORMS = `import { inputProps } from 'basalt-ui/forms'\n`
3377
+
3378
+ it('flags a spread inputProps() with no sibling key', () => {
3379
+ const { code, rules, output } = run(
3380
+ `${FORMS}export const C = () => <TextInput {...inputProps(form, 'email')} />\n`,
3381
+ )
3382
+ expect(code).toBe(1)
3383
+ expect(rules).toContain('forms-field-key')
3384
+ expect(output).toContain('no sibling `key`')
3385
+ })
3386
+
3387
+ it('does NOT flag the documented two-call idiom', () => {
3388
+ const { code, rules } = run(
3389
+ `import { fieldKey, inputProps } from 'basalt-ui/forms'\n` +
3390
+ `export const C = () => <TextInput key={fieldKey(form, 'e')} {...inputProps(form, 'e')} />\n`,
3391
+ )
3392
+ expect(code).toBe(0)
3393
+ expect(rules).not.toContain('forms-field-key')
3394
+ })
3395
+
3396
+ // An `inputProps` a consumer wrote themselves is not basalt's — the same provenance gate the
3397
+ // control and chart rules take, one lane over.
3398
+ it('does NOT flag a same-named helper imported from somewhere else', () => {
3399
+ const { code, rules } = run(
3400
+ `import { inputProps } from './my-helpers'\n` +
3401
+ `export const C = () => <TextInput {...inputProps(form, 'email')} />\n`,
3402
+ )
3403
+ expect(code).toBe(0)
3404
+ expect(rules).not.toContain('forms-field-key')
3405
+ })
3406
+
3407
+ it('resolves an aliased import — `inputProps as f` is still inputProps', () => {
3408
+ const { code, rules } = run(
3409
+ `import { inputProps as f } from 'basalt-ui/forms'\n` +
3410
+ `export const C = () => <TextInput {...f(form, 'email')} />\n`,
3411
+ )
3412
+ expect(code).toBe(1)
3413
+ expect(rules).toContain('forms-field-key')
3414
+ })
3415
+
3416
+ // The second message: `field` still RETURNS `key`, so it is reported whether or not the element
3417
+ // carries one — the remedy is the rename, never an added attribute.
3418
+ it('flags a spread field() even when the element already has a key', () => {
3419
+ const { code, rules, output } = run(
3420
+ `import { field } from 'basalt-ui/forms'\n` +
3421
+ `export const C = () => <TextInput key="e" {...field(form, 'email')} />\n`,
3422
+ )
3423
+ expect(code).toBe(1)
3424
+ expect(rules).toContain('forms-field-key')
3425
+ expect(output).toContain('@deprecated 1.27 alias')
3426
+ })
3427
+
3428
+ it('honours a theme-allow naming the rule', () => {
3429
+ const { code, rules } = run(
3430
+ `${FORMS}export const C = () => (\n` +
3431
+ ` // theme-allow forms-field-key — this input is remounted by its parent\n` +
3432
+ ` <TextInput {...inputProps(form, 'email')} />\n)\n`,
3433
+ )
3434
+ expect(code).toBe(0)
3435
+ expect(rules).not.toContain('forms-field-key')
3436
+ })
3437
+
3438
+ // The autofix has to produce COMPILING code: an inserted `fieldKey(…)` with no import is a worse
3439
+ // outcome than the missing key it replaced.
3440
+ it('autofixes the key in, with the same arguments, and adds fieldKey to the import once', () => {
3441
+ const source =
3442
+ `${FORMS}export const C = () => (\n` +
3443
+ ` <form>\n` +
3444
+ ` <TextInput {...inputProps(form, 'email')} label="Email" />\n` +
3445
+ ` <TextInput {...inputProps(form, 'name')} />\n` +
3446
+ ` </form>\n)\n`
3447
+ writeFileSync(resolve(dir, 'fixture.tsx'), source)
3448
+ Bun.spawnSync([OXLINT_BIN, '-c', '.oxlintrc.json', '--fix', 'fixture.tsx'], { cwd: dir })
3449
+ const fixed = readFileSync(resolve(dir, 'fixture.tsx'), 'utf8')
3450
+ expect(fixed).toContain(`import { inputProps, fieldKey } from 'basalt-ui/forms'`)
3451
+ expect(fixed).toContain(`key={fieldKey(form, 'email')} {...inputProps(form, 'email')}`)
3452
+ expect(fixed).toContain(`key={fieldKey(form, 'name')} {...inputProps(form, 'name')}`)
3453
+ // Once, not twice: two overlapping edits to one import declaration would drop one of them.
3454
+ expect(fixed.match(/fieldKey,|, fieldKey/g)).toHaveLength(1)
3455
+ })
3456
+
3457
+ it('does not re-add fieldKey when it is already imported', () => {
3458
+ const source =
3459
+ `import { fieldKey, inputProps } from 'basalt-ui/forms'\n` +
3460
+ `export const C = () => <TextInput {...inputProps(form, 'email')} />\n`
3461
+ writeFileSync(resolve(dir, 'fixture.tsx'), source)
3462
+ Bun.spawnSync([OXLINT_BIN, '-c', '.oxlintrc.json', '--fix', 'fixture.tsx'], { cwd: dir })
3463
+ const fixed = readFileSync(resolve(dir, 'fixture.tsx'), 'utf8')
3464
+ expect(fixed).toContain(`import { fieldKey, inputProps } from 'basalt-ui/forms'`)
3465
+ expect(fixed).toContain(`key={fieldKey(form, 'email')}`)
3466
+ })
3467
+ })
3468
+
3469
+ // ── the deprecation ledger (B4) ──────────────────────────────────────────────
3470
+
3471
+ describe('DEPRECATED_EXPORTS', () => {
3472
+ const pkgVersion = (
3473
+ JSON.parse(readFileSync(resolve(import.meta.dirname, '..', 'package.json'), 'utf8')) as {
3474
+ version: string
3475
+ }
3476
+ ).version
3477
+
3478
+ it('carries a replacement and a removeIn at least one minor out on every row', () => {
3479
+ const [major, minor] = pkgVersion.split('.').map(Number)
3480
+ const floor = `${major}.${(minor as number) + 1}.0`
3481
+ for (const row of DEPRECATED_EXPORTS) {
3482
+ expect([row.name, row.replacement.length > 0]).toEqual([row.name, true])
3483
+ expect([row.name, /^\d+\.\d+\.\d+$/.test(row.removeIn)]).toEqual([row.name, true])
3484
+ expect([row.name, compareSemver(row.removeIn, floor) >= 0]).toEqual([row.name, true])
3485
+ }
3486
+ })
3487
+
3488
+ it('names a real published subpath on every row', () => {
3489
+ const exports = Object.keys(
3490
+ (
3491
+ JSON.parse(readFileSync(resolve(import.meta.dirname, '..', 'package.json'), 'utf8')) as {
3492
+ exports: Record<string, unknown>
3493
+ }
3494
+ ).exports,
3495
+ )
3496
+ for (const row of DEPRECATED_EXPORTS) {
3497
+ const subpath = row.subpath === 'basalt-ui' ? '.' : row.subpath.replace('basalt-ui', '.')
3498
+ expect([row.subpath, exports.includes(subpath)]).toEqual([row.subpath, true])
3499
+ }
3500
+ })
3501
+
3502
+ /**
3503
+ * The barrel half of the lifecycle: a JSDoc `@deprecated` on a published barrel with no ledger
3504
+ * row is an export nothing nudges anyone off. Deliberately scoped to `src/**' + '/index.ts(x)` —
3505
+ * the files a consumer's import resolves through. To add one: write the `@deprecated` JSDoc, add
3506
+ * the `DEPRECATED_EXPORTS` row, add the `MIGRATING.md` row.
3507
+ */
3508
+ it('has a row for every @deprecated JSDoc tag on a published barrel', () => {
3509
+ const barrels = [
3510
+ ...new Bun.Glob('src/**/index.{ts,tsx}').scanSync({
3511
+ cwd: resolve(import.meta.dirname, '..'),
3512
+ absolute: true,
3513
+ }),
3514
+ ]
3515
+ const names = new Set(DEPRECATED_EXPORTS.map((row) => row.prop ?? row.name))
3516
+ for (const file of barrels) {
3517
+ const lines = readFileSync(file, 'utf8').split('\n')
3518
+ for (const [index, line] of lines.entries()) {
3519
+ if (!/^\s*\*\s*@deprecated\b/.test(line)) continue
3520
+ const declared = lines
3521
+ .slice(index, index + 12)
3522
+ .map((l) => /^\s*(?:readonly\s+)?([A-Za-z_$][\w$]*)\??\s*[:(]/.exec(l)?.[1])
3523
+ .find((name) => name !== undefined)
3524
+ expect([file, declared, declared !== undefined && names.has(declared)]).toEqual([
3525
+ file,
3526
+ declared,
3527
+ true,
3528
+ ])
3529
+ }
3530
+ }
3531
+ })
3532
+ })
3533
+
3534
+ // ── the `Ships:` claim above each rule (F26) ─────────────────────────────────
3535
+
3536
+ /**
3537
+ * F26 was a rule's own doc comment claiming a level it did not ship at, with nothing able to catch
3538
+ * it: the C16 ledger is asserted against the preset, and a COMMENT is outside the ledger. So the
3539
+ * claim is now one normalised line — `// Ships: <level>` directly above each rule's `const` — and
3540
+ * this is the assertion that makes it as checkable as the ledger it restates.
3541
+ */
3542
+ describe('the `Ships:` line above each rule', () => {
3543
+ const source = readFileSync(resolve(import.meta.dirname, 'oxlint-plugin.js'), 'utf8')
3544
+ const lines = source.split('\n')
3545
+ const shipped = JSON.parse(readFileSync(resolve(import.meta.dirname, 'oxlint.json'), 'utf8')) as {
3546
+ rules: Record<string, unknown>
3547
+ }
3548
+ // The registry block maps rule id → the `const` implementing it.
3549
+ const registry = [
3550
+ ...source.slice(source.indexOf('export default {')).matchAll(/^ {4}'([\w-]+)': (\w+),$/gm),
3551
+ ].map(([, id, binding]) => ({ id: id as string, binding: binding as string }))
3552
+
3553
+ it('covers every registered rule', () => {
3554
+ expect(registry.map((r) => r.id).toSorted()).toEqual(Object.keys(basaltPlugin.rules).toSorted())
3555
+ })
3556
+
3557
+ for (const { id, binding } of registry) {
3558
+ it(`${id}: the claim matches oxlint.json`, () => {
3559
+ const at = lines.indexOf(`const ${binding} = {`)
3560
+ expect([id, at]).not.toEqual([id, -1])
3561
+ const claim = /^\/\/ Ships: (.+)$/.exec(lines[at - 1] as string)?.[1]
3562
+ const level = shipped.rules[`basalt/${id}`]
3563
+ if (level === undefined) {
3564
+ expect([id, claim]).toEqual([id, 'repo-local only'])
3565
+ return
3566
+ }
3567
+ if (Object.hasOwn(PLUGIN_RULE_GRACE, id)) {
3568
+ const { promote } = PLUGIN_RULE_GRACE[id] as GraceEntry
3569
+ expect([id, claim]).toEqual([id, `warn (grace → ${promote})`])
3570
+ expect([id, level]).toEqual([id, 'warn'])
3571
+ return
3572
+ }
3573
+ if (Object.hasOwn(PLUGIN_RULE_ADVISORY, id)) {
3574
+ expect([id, claim]).toEqual([id, 'warn (advisory)'])
3575
+ expect([id, level]).toEqual([id, 'warn'])
3576
+ return
3577
+ }
3578
+ expect([id, claim]).toEqual([id, 'error'])
3579
+ expect([id, level]).toEqual([id, 'error'])
3580
+ })
3581
+ }
3582
+ })