basalt-ui 1.28.0 → 1.29.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 (455) hide show
  1. package/AGENTS.md +11 -18
  2. package/CHANGELOG.md +13 -0
  3. package/MIGRATING.md +151 -7
  4. package/README.md +23 -66
  5. package/agent/rules/basalt-batteries.md +59 -182
  6. package/agent/rules/basalt-charts.md +56 -75
  7. package/agent/rules/basalt-controls.md +38 -69
  8. package/agent/rules/basalt-mantine.md +58 -103
  9. package/agent/rules/basalt-state.md +42 -60
  10. package/agent/rules/basalt-tokens.md +46 -67
  11. package/agent/skills/basalt-app/SKILL.md +2 -0
  12. package/agent/templates/CLAUDE-block.md.tpl +14 -14
  13. package/configs/oxlint-plugin.js +70 -167
  14. package/configs/oxlint-plugin.test.ts +20 -186
  15. package/configs/oxlint.json +0 -1
  16. package/dist/agent/ai-sdk-transport.d.ts.map +1 -1
  17. package/dist/agent/ai-sdk-transport.js +7 -4
  18. package/dist/agent/coalesce.js +1 -1
  19. package/dist/agent/merge.js +1 -1
  20. package/dist/agent/transport.d.ts +13 -2
  21. package/dist/agent/transport.d.ts.map +1 -1
  22. package/dist/agent/use-agent-thread-runs.d.ts +12 -1
  23. package/dist/agent/use-agent-thread-runs.d.ts.map +1 -1
  24. package/dist/agent/use-agent-thread-runs.js +20 -6
  25. package/dist/agent-chat/index.d.ts +1 -1
  26. package/dist/agent-chat/index.d.ts.map +1 -1
  27. package/dist/agent-chat/relative-time.js +1 -1
  28. package/dist/agent-chat/thread-detail-panel.js +1 -1
  29. package/dist/agent-chat/thread-feed-row.d.ts +32 -7
  30. package/dist/agent-chat/thread-feed-row.d.ts.map +1 -1
  31. package/dist/agent-chat/thread-feed-row.js +121 -86
  32. package/dist/agent-chat/thread-feed.d.ts +11 -1
  33. package/dist/agent-chat/thread-feed.d.ts.map +1 -1
  34. package/dist/agent-chat/thread-feed.js +10 -6
  35. package/dist/agent-chat/thread-message.js +1 -1
  36. package/dist/agent-chat/virtualize.d.ts +39 -0
  37. package/dist/agent-chat/virtualize.d.ts.map +1 -1
  38. package/dist/agent-chat/virtualize.js +15 -0
  39. package/dist/charts/index.d.ts +3 -3
  40. package/dist/charts/index.d.ts.map +1 -1
  41. package/dist/charts/index.js +8 -4
  42. package/dist/charts/kinds/BandStrip.js +1 -1
  43. package/dist/charts/kinds/Bars.d.ts +1 -3
  44. package/dist/charts/kinds/Bars.d.ts.map +1 -1
  45. package/dist/charts/kinds/Bars.js +1 -1
  46. package/dist/charts/kinds/DualPanel.d.ts +3 -1
  47. package/dist/charts/kinds/DualPanel.d.ts.map +1 -1
  48. package/dist/charts/kinds/DualPanel.js +1 -1
  49. package/dist/charts/kinds/StackedArea.js +1 -1
  50. package/dist/charts/kinds/ZonedLine.d.ts +1 -3
  51. package/dist/charts/kinds/ZonedLine.d.ts.map +1 -1
  52. package/dist/charts/primitives/Axes.d.ts +21 -2
  53. package/dist/charts/primitives/Axes.d.ts.map +1 -1
  54. package/dist/charts/primitives/Axes.js +34 -2
  55. package/dist/charts/primitives/ChartCard.d.ts +20 -4
  56. package/dist/charts/primitives/ChartCard.d.ts.map +1 -1
  57. package/dist/charts/primitives/ChartCard.js +62 -33
  58. package/dist/charts/primitives/ChartFrame.d.ts.map +1 -1
  59. package/dist/charts/primitives/ChartFrame.js +8 -1
  60. package/dist/charts/primitives/ChartPending.d.ts +12 -6
  61. package/dist/charts/primitives/ChartPending.d.ts.map +1 -1
  62. package/dist/charts/primitives/ChartPending.js +1 -1
  63. package/dist/charts/utils/format.d.ts +5 -92
  64. package/dist/charts/utils/format.d.ts.map +1 -1
  65. package/dist/charts/utils/format.js +10 -80
  66. package/dist/cli/audit-allows.d.ts +24 -0
  67. package/dist/cli/audit-allows.d.ts.map +1 -0
  68. package/dist/cli/audit-allows.js +240 -0
  69. package/dist/cli/check-theme.d.ts +10 -0
  70. package/dist/cli/check-theme.d.ts.map +1 -0
  71. package/dist/cli/check-theme.js +139 -0
  72. package/dist/cli/doctor.d.ts +47 -0
  73. package/dist/cli/doctor.d.ts.map +1 -0
  74. package/dist/cli/doctor.js +197 -0
  75. package/dist/cli/guard-hook.d.ts +9 -0
  76. package/dist/cli/guard-hook.d.ts.map +1 -0
  77. package/dist/cli/guard-hook.js +93 -0
  78. package/dist/cli/help.d.ts +9 -0
  79. package/dist/cli/help.d.ts.map +1 -0
  80. package/dist/cli/help.js +53 -0
  81. package/dist/cli/index.d.ts +360 -270
  82. package/dist/cli/index.d.ts.map +1 -1
  83. package/dist/cli/index.js +85 -1901
  84. package/dist/cli/init.d.ts +4 -0
  85. package/dist/cli/init.d.ts.map +1 -0
  86. package/dist/cli/init.js +165 -0
  87. package/dist/cli/sync.d.ts +43 -0
  88. package/dist/cli/sync.d.ts.map +1 -0
  89. package/dist/cli/sync.js +206 -0
  90. package/dist/cli/tokens-css.d.ts +51 -0
  91. package/dist/cli/tokens-css.d.ts.map +1 -0
  92. package/dist/cli/tokens-css.js +165 -0
  93. package/dist/commands/define-commands.js +1 -1
  94. package/dist/commands/define-overlays.js +1 -1
  95. package/dist/commands/index.d.ts +2 -1
  96. package/dist/commands/index.d.ts.map +1 -1
  97. package/dist/commands/index.js +6 -0
  98. package/dist/commands/overlays-mount.d.ts +7 -9
  99. package/dist/commands/overlays-mount.d.ts.map +1 -1
  100. package/dist/commands/shell-bridge.d.ts +57 -0
  101. package/dist/commands/shell-bridge.d.ts.map +1 -0
  102. package/dist/commands/shell-bridge.js +32 -0
  103. package/dist/common/errors.d.ts +35 -0
  104. package/dist/common/errors.d.ts.map +1 -1
  105. package/dist/common/errors.js +29 -1
  106. package/dist/common/index.d.ts +2 -0
  107. package/dist/common/index.d.ts.map +1 -1
  108. package/dist/common/index.js +6 -0
  109. package/dist/common/is-dev.d.ts.map +1 -0
  110. package/dist/{motion/index.d.ts → common/motion.d.ts} +1 -1
  111. package/dist/common/motion.d.ts.map +1 -0
  112. package/dist/common/props.d.ts +1 -1
  113. package/dist/common/props.d.ts.map +1 -1
  114. package/dist/common/validate.js +1 -1
  115. package/dist/content/markdown.d.ts +3 -4
  116. package/dist/content/markdown.d.ts.map +1 -1
  117. package/dist/content/markdown.js +1 -4
  118. package/dist/controls/actions.d.ts +1 -1
  119. package/dist/controls/actions.d.ts.map +1 -1
  120. package/dist/controls/panel-row.js +1 -1
  121. package/dist/controls/range-filter.js +1 -1
  122. package/dist/dashboard/delta-badge.d.ts +18 -10
  123. package/dist/dashboard/delta-badge.d.ts.map +1 -1
  124. package/dist/dashboard/delta-badge.js +33 -1
  125. package/dist/dashboard/empty-state.d.ts +1 -4
  126. package/dist/dashboard/empty-state.d.ts.map +1 -1
  127. package/dist/dashboard/empty-state.js +1 -2
  128. package/dist/dashboard/index.d.ts +4 -2
  129. package/dist/dashboard/index.d.ts.map +1 -1
  130. package/dist/dashboard/index.js +7 -1
  131. package/dist/dashboard/query-state.d.ts +4 -13
  132. package/dist/dashboard/query-state.d.ts.map +1 -1
  133. package/dist/dashboard/query-state.js +4 -7
  134. package/dist/{section → dashboard}/section.d.ts +2 -2
  135. package/dist/{section → dashboard}/section.d.ts.map +1 -1
  136. package/dist/{section → dashboard}/section.js +2 -2
  137. package/dist/dashboard/settings-section.d.ts +1 -1
  138. package/dist/dashboard/settings-section.d.ts.map +1 -1
  139. package/dist/dashboard/settings-section.js +1 -1
  140. package/dist/dashboard/stat-card.d.ts +1 -1
  141. package/dist/dashboard/stat-card.d.ts.map +1 -1
  142. package/dist/dashboard/stat-card.js +1 -1
  143. package/dist/dashboard/widget-header.d.ts.map +1 -0
  144. package/dist/data/data-table.js +2 -2
  145. package/dist/data/table.d.ts +1 -1
  146. package/dist/data/table.d.ts.map +1 -1
  147. package/dist/data/table.js +2 -16
  148. package/dist/data/virtual.d.ts +3 -3
  149. package/dist/dev-dock.d.ts +28 -0
  150. package/dist/dev-dock.d.ts.map +1 -0
  151. package/dist/dev-dock.js +85 -0
  152. package/dist/format/date.d.ts +62 -0
  153. package/dist/format/date.d.ts.map +1 -0
  154. package/dist/format/date.js +106 -0
  155. package/dist/format/index.d.ts +39 -0
  156. package/dist/format/index.d.ts.map +1 -0
  157. package/dist/format/index.js +54 -0
  158. package/dist/format/measure.d.ts +15 -0
  159. package/dist/format/measure.d.ts.map +1 -0
  160. package/dist/format/measure.js +16 -0
  161. package/dist/format/number.d.ts +69 -0
  162. package/dist/format/number.d.ts.map +1 -0
  163. package/dist/format/number.js +62 -0
  164. package/dist/format/shared.d.ts +27 -0
  165. package/dist/format/shared.d.ts.map +1 -0
  166. package/dist/format/shared.js +29 -0
  167. package/dist/forms/field.d.ts +4 -20
  168. package/dist/forms/field.d.ts.map +1 -1
  169. package/dist/forms/field.js +0 -4
  170. package/dist/forms/form-layout.d.ts +1 -1
  171. package/dist/forms/form-layout.d.ts.map +1 -1
  172. package/dist/forms/form-layout.js +1 -1
  173. package/dist/forms/index.d.ts +1 -1
  174. package/dist/forms/index.d.ts.map +1 -1
  175. package/dist/forms/index.js +1 -2
  176. package/dist/forms/use-form-submit.js +1 -1
  177. package/dist/guard/index.d.ts +2 -9
  178. package/dist/guard/index.d.ts.map +1 -1
  179. package/dist/guard/index.js +16 -26
  180. package/dist/guard/types.d.ts +8 -10
  181. package/dist/guard/types.d.ts.map +1 -1
  182. package/dist/index.d.ts +16 -11
  183. package/dist/index.d.ts.map +1 -1
  184. package/dist/index.js +23 -29
  185. package/dist/notifications/index.d.ts +5 -5
  186. package/dist/notifications/index.d.ts.map +1 -1
  187. package/dist/notifications/index.js +0 -2
  188. package/dist/notifications/mount-guard.d.ts.map +1 -1
  189. package/dist/notifications/mount-guard.js +2 -2
  190. package/dist/provider/connectivity-indicator.d.ts.map +1 -0
  191. package/dist/{connectivity → provider}/connectivity-provider.d.ts +1 -1
  192. package/dist/provider/connectivity-provider.d.ts.map +1 -0
  193. package/dist/{connectivity/types.d.ts → provider/connectivity-types.d.ts} +1 -1
  194. package/dist/provider/connectivity-types.d.ts.map +1 -0
  195. package/dist/provider/error-fallback.module.css +21 -0
  196. package/dist/provider/error-fallback.module.css.d.ts +9 -0
  197. package/dist/provider/index.d.ts +20 -36
  198. package/dist/provider/index.d.ts.map +1 -1
  199. package/dist/provider/index.js +42 -32
  200. package/dist/provider/lab-theme.js +1 -1
  201. package/dist/provider/use-connectivity.d.ts +3 -0
  202. package/dist/provider/use-connectivity.d.ts.map +1 -0
  203. package/dist/query-client.d.ts +63 -0
  204. package/dist/query-client.d.ts.map +1 -0
  205. package/dist/query-client.js +33 -0
  206. package/dist/{query/devtools.d.ts → query-devtools.d.ts} +2 -2
  207. package/dist/query-devtools.d.ts.map +1 -0
  208. package/dist/register.d.ts +17 -3
  209. package/dist/register.d.ts.map +1 -1
  210. package/dist/router-tanstack/index.d.ts +4 -7
  211. package/dist/router-tanstack/index.d.ts.map +1 -1
  212. package/dist/router-tanstack/index.js +1 -9
  213. package/dist/router-tanstack/nav.d.ts +1 -1
  214. package/dist/router-tanstack/nav.d.ts.map +1 -1
  215. package/dist/router-tanstack/search-store.d.ts +3 -6
  216. package/dist/router-tanstack/search-store.d.ts.map +1 -1
  217. package/dist/router-tanstack/use-nav.d.ts +1 -1
  218. package/dist/router-tanstack/use-nav.d.ts.map +1 -1
  219. package/dist/shell/app-breadcrumbs.d.ts +1 -1
  220. package/dist/shell/app-breadcrumbs.d.ts.map +1 -1
  221. package/dist/shell/app-main.module.css +11 -0
  222. package/dist/shell/app-mobile-nav.d.ts +1 -1
  223. package/dist/shell/app-mobile-nav.d.ts.map +1 -1
  224. package/dist/shell/app-mobile-nav.js +24 -2
  225. package/dist/shell/app-sidebar.d.ts +1 -1
  226. package/dist/shell/app-sidebar.d.ts.map +1 -1
  227. package/dist/shell/app-sidebar.js +31 -2
  228. package/dist/shell/app-sidebar.module.css +10 -0
  229. package/dist/shell/index.d.ts +10 -4
  230. package/dist/shell/index.d.ts.map +1 -1
  231. package/dist/shell/index.js +13 -17
  232. package/dist/shell/mobile-nav-model.d.ts +1 -1
  233. package/dist/shell/mobile-nav-model.d.ts.map +1 -1
  234. package/dist/{nav/types.d.ts → shell/nav-types.d.ts} +1 -1
  235. package/dist/shell/nav-types.d.ts.map +1 -0
  236. package/dist/shell/page-aside.d.ts.map +1 -1
  237. package/dist/shell/page-aside.js +2 -29
  238. package/dist/shell/page-title.d.ts +14 -0
  239. package/dist/shell/page-title.d.ts.map +1 -0
  240. package/dist/shell/page-title.js +25 -0
  241. package/dist/shell/page-title.module.css +33 -0
  242. package/dist/shell/page-title.module.css.d.ts +11 -0
  243. package/dist/shell/sidebar-block-model.d.ts +1 -1
  244. package/dist/shell/sidebar-block-model.d.ts.map +1 -1
  245. package/dist/shell/sidebar-blocks.d.ts +1 -1
  246. package/dist/shell/sidebar-blocks.d.ts.map +1 -1
  247. package/dist/shell/use-breakpoint.d.ts +11 -0
  248. package/dist/shell/use-breakpoint.d.ts.map +1 -0
  249. package/dist/shell/use-breakpoint.js +33 -0
  250. package/dist/state/fields.d.ts +3 -19
  251. package/dist/state/fields.d.ts.map +1 -1
  252. package/dist/state/fields.js +6 -16
  253. package/dist/styles.css +36 -0
  254. package/dist/surfaces.d.ts +18 -41
  255. package/dist/surfaces.d.ts.map +1 -1
  256. package/dist/surfaces.js +46 -59
  257. package/dist/theme/index.d.ts.map +1 -1
  258. package/dist/{theme-toggle/index.d.ts → theme/theme-toggle.d.ts} +1 -1
  259. package/dist/theme/theme-toggle.d.ts.map +1 -0
  260. package/dist/{theme-toggle/index.js → theme/theme-toggle.js} +1 -1
  261. package/dist/tokens/index.d.ts +1 -0
  262. package/dist/tokens/index.d.ts.map +1 -1
  263. package/dist/tokens/index.js +10 -0
  264. package/dist/tokens/palette.d.ts +12 -0
  265. package/dist/tokens/palette.d.ts.map +1 -1
  266. package/dist/tokens/palette.js +13 -1
  267. package/dist/tokens.css +1 -0
  268. package/llms.txt +9 -24
  269. package/package.json +12 -16
  270. package/src/agent/ai-sdk-transport.ts +11 -4
  271. package/src/agent/coalesce.ts +1 -1
  272. package/src/agent/merge.ts +1 -1
  273. package/src/agent/transport.ts +15 -2
  274. package/src/agent/use-agent-thread-runs.ts +65 -15
  275. package/src/agent-chat/index.ts +6 -4
  276. package/src/agent-chat/relative-time.ts +1 -1
  277. package/src/agent-chat/thread-detail-panel.tsx +1 -1
  278. package/src/agent-chat/thread-feed-row.tsx +105 -30
  279. package/src/agent-chat/thread-feed.tsx +51 -25
  280. package/src/agent-chat/thread-message.tsx +1 -1
  281. package/src/agent-chat/virtualize.ts +48 -0
  282. package/src/charts/index.ts +7 -10
  283. package/src/charts/kinds/BandStrip.tsx +1 -1
  284. package/src/charts/kinds/Bars.tsx +2 -5
  285. package/src/charts/kinds/DualPanel.tsx +4 -3
  286. package/src/charts/kinds/StackedArea.tsx +1 -1
  287. package/src/charts/kinds/ZonedLine.tsx +1 -4
  288. package/src/charts/primitives/Axes.tsx +49 -0
  289. package/src/charts/primitives/ChartCard.tsx +49 -6
  290. package/src/charts/primitives/ChartFrame.tsx +5 -1
  291. package/src/charts/primitives/ChartPending.tsx +14 -8
  292. package/src/charts/utils/format.ts +17 -186
  293. package/src/cli/audit-allows.ts +371 -0
  294. package/src/cli/check-theme.ts +197 -0
  295. package/src/cli/doctor.ts +331 -0
  296. package/src/cli/guard-hook.ts +115 -0
  297. package/src/cli/help.ts +101 -0
  298. package/src/cli/index.ts +269 -3439
  299. package/src/cli/init.ts +247 -0
  300. package/src/cli/sync.ts +324 -0
  301. package/src/cli/tokens-css.ts +252 -0
  302. package/src/commands/define-commands.ts +1 -1
  303. package/src/commands/define-overlays.ts +1 -1
  304. package/src/commands/index.ts +10 -1
  305. package/src/commands/overlays-mount.tsx +8 -10
  306. package/src/commands/shell-bridge.ts +86 -0
  307. package/src/common/errors.ts +70 -0
  308. package/src/common/index.ts +2 -0
  309. package/src/common/props.ts +1 -1
  310. package/src/common/validate.ts +1 -1
  311. package/src/content/markdown.tsx +4 -8
  312. package/src/controls/actions.tsx +1 -1
  313. package/src/controls/panel-row.tsx +1 -1
  314. package/src/controls/range-filter.tsx +1 -1
  315. package/src/dashboard/delta-badge.tsx +74 -7
  316. package/src/dashboard/empty-state.tsx +2 -6
  317. package/src/dashboard/index.ts +11 -2
  318. package/src/dashboard/query-state.tsx +5 -18
  319. package/src/{section → dashboard}/section.tsx +4 -4
  320. package/src/dashboard/settings-section.tsx +2 -2
  321. package/src/dashboard/stat-card.tsx +2 -2
  322. package/src/data/data-table.tsx +3 -3
  323. package/src/data/table.ts +6 -25
  324. package/src/data/virtual-list.tsx +1 -1
  325. package/src/data/virtual.ts +3 -3
  326. package/src/dev-dock.tsx +159 -0
  327. package/src/format/date.ts +184 -0
  328. package/src/format/index.ts +69 -0
  329. package/src/format/measure.ts +31 -0
  330. package/src/format/number.ts +129 -0
  331. package/src/format/shared.ts +63 -0
  332. package/src/forms/field.ts +4 -24
  333. package/src/forms/form-layout.tsx +2 -2
  334. package/src/forms/index.ts +1 -2
  335. package/src/forms/use-form-submit.ts +1 -1
  336. package/src/guard/index.ts +30 -56
  337. package/src/guard/types.ts +7 -10
  338. package/src/index.ts +38 -29
  339. package/src/notifications/index.ts +5 -7
  340. package/src/notifications/mount-guard.ts +7 -9
  341. package/src/{connectivity → provider}/connectivity-indicator.tsx +1 -1
  342. package/src/{connectivity → provider}/connectivity-provider.tsx +1 -1
  343. package/src/provider/error-fallback.module.css +21 -0
  344. package/src/provider/error-fallback.module.css.d.ts +9 -0
  345. package/src/provider/index.tsx +71 -71
  346. package/src/provider/lab-theme.ts +1 -1
  347. package/src/{connectivity → provider}/use-connectivity.ts +3 -3
  348. package/src/query-client.ts +100 -0
  349. package/src/{query/devtools.tsx → query-devtools.tsx} +5 -4
  350. package/src/register.ts +20 -4
  351. package/src/router-tanstack/index.ts +4 -15
  352. package/src/router-tanstack/nav.ts +1 -1
  353. package/src/router-tanstack/search-store.ts +3 -4
  354. package/src/router-tanstack/use-nav.ts +1 -1
  355. package/src/shell/app-breadcrumbs.tsx +1 -1
  356. package/src/shell/app-main.module.css +11 -0
  357. package/src/shell/app-mobile-nav.tsx +28 -1
  358. package/src/shell/app-sidebar.module.css +10 -0
  359. package/src/shell/app-sidebar.tsx +31 -2
  360. package/src/shell/index.tsx +23 -28
  361. package/src/shell/mobile-nav-model.ts +1 -1
  362. package/src/shell/page-aside.tsx +2 -52
  363. package/src/shell/page-title.module.css +33 -0
  364. package/src/shell/page-title.module.css.d.ts +11 -0
  365. package/src/shell/page-title.tsx +57 -0
  366. package/src/shell/sidebar-block-model.ts +1 -1
  367. package/src/shell/sidebar-blocks.tsx +1 -1
  368. package/src/shell/use-breakpoint.ts +73 -0
  369. package/src/state/fields.ts +7 -43
  370. package/src/styles.css +36 -0
  371. package/src/surfaces.ts +45 -60
  372. package/src/theme/index.ts +7 -0
  373. package/src/{theme-toggle/index.tsx → theme/theme-toggle.tsx} +1 -1
  374. package/src/tokens/index.ts +10 -0
  375. package/src/tokens/palette.ts +12 -0
  376. package/dist/connectivity/connectivity-indicator.d.ts.map +0 -1
  377. package/dist/connectivity/connectivity-provider.d.ts.map +0 -1
  378. package/dist/connectivity/index.d.ts +0 -14
  379. package/dist/connectivity/index.d.ts.map +0 -1
  380. package/dist/connectivity/index.js +0 -8
  381. package/dist/connectivity/types.d.ts.map +0 -1
  382. package/dist/connectivity/use-connectivity.d.ts +0 -3
  383. package/dist/connectivity/use-connectivity.d.ts.map +0 -1
  384. package/dist/data/index.d.ts +0 -38
  385. package/dist/data/index.d.ts.map +0 -1
  386. package/dist/data/index.js +0 -24
  387. package/dist/motion/index.d.ts.map +0 -1
  388. package/dist/nav/types.d.ts.map +0 -1
  389. package/dist/notifications/overlay.d.ts +0 -20
  390. package/dist/notifications/overlay.d.ts.map +0 -1
  391. package/dist/notifications/overlay.js +0 -14
  392. package/dist/query/devtools.d.ts.map +0 -1
  393. package/dist/query/error-message.d.ts +0 -41
  394. package/dist/query/error-message.d.ts.map +0 -1
  395. package/dist/query/error-message.js +0 -30
  396. package/dist/query/index.d.ts +0 -47
  397. package/dist/query/index.d.ts.map +0 -1
  398. package/dist/query/index.js +0 -56
  399. package/dist/router-tanstack/field.d.ts +0 -14
  400. package/dist/router-tanstack/field.d.ts.map +0 -1
  401. package/dist/router-tanstack/field.js +0 -4
  402. package/dist/router-tanstack/multi-search-param-store.d.ts +0 -43
  403. package/dist/router-tanstack/multi-search-param-store.d.ts.map +0 -1
  404. package/dist/router-tanstack/multi-search-param-store.js +0 -30
  405. package/dist/router-tanstack/search-param-store.d.ts +0 -42
  406. package/dist/router-tanstack/search-param-store.d.ts.map +0 -1
  407. package/dist/router-tanstack/search-param-store.js +0 -25
  408. package/dist/section/index.d.ts +0 -2
  409. package/dist/section/index.d.ts.map +0 -1
  410. package/dist/section/index.js +0 -4
  411. package/dist/theme-toggle/index.d.ts.map +0 -1
  412. package/dist/utils/is-dev.d.ts.map +0 -1
  413. package/dist/widget-header/delta-badge.d.ts +0 -19
  414. package/dist/widget-header/delta-badge.d.ts.map +0 -1
  415. package/dist/widget-header/delta-badge.js +0 -36
  416. package/dist/widget-header/index.d.ts +0 -3
  417. package/dist/widget-header/index.d.ts.map +0 -1
  418. package/dist/widget-header/index.js +0 -8
  419. package/dist/widget-header/widget-header.d.ts.map +0 -1
  420. package/src/connectivity/index.ts +0 -19
  421. package/src/data/index.ts +0 -69
  422. package/src/notifications/overlay.tsx +0 -66
  423. package/src/query/error-message.ts +0 -70
  424. package/src/query/index.ts +0 -97
  425. package/src/router-tanstack/field.ts +0 -36
  426. package/src/router-tanstack/multi-search-param-store.ts +0 -106
  427. package/src/router-tanstack/search-param-store.ts +0 -100
  428. package/src/section/index.ts +0 -1
  429. package/src/widget-header/delta-badge.tsx +0 -77
  430. package/src/widget-header/index.ts +0 -10
  431. /package/dist/{utils → common}/is-dev.d.ts +0 -0
  432. /package/dist/{utils → common}/is-dev.js +0 -0
  433. /package/dist/{motion/index.js → common/motion.js} +0 -0
  434. /package/dist/{section → dashboard}/section.module.css +0 -0
  435. /package/dist/{section → dashboard}/section.module.css.d.ts +0 -0
  436. /package/dist/{widget-header → dashboard}/widget-header.d.ts +0 -0
  437. /package/dist/{widget-header → dashboard}/widget-header.js +0 -0
  438. /package/dist/{widget-header → dashboard}/widget-header.module.css +0 -0
  439. /package/dist/{widget-header → dashboard}/widget-header.module.css.d.ts +0 -0
  440. /package/dist/{connectivity → provider}/connectivity-indicator.d.ts +0 -0
  441. /package/dist/{connectivity → provider}/connectivity-indicator.js +0 -0
  442. /package/dist/{connectivity → provider}/connectivity-provider.js +0 -0
  443. /package/dist/{connectivity/types.js → provider/connectivity-types.js} +0 -0
  444. /package/dist/{connectivity → provider}/use-connectivity.js +0 -0
  445. /package/dist/{query/devtools.js → query-devtools.js} +0 -0
  446. /package/dist/{nav/types.js → shell/nav-types.js} +0 -0
  447. /package/src/{utils → common}/is-dev.ts +0 -0
  448. /package/src/{motion/index.ts → common/motion.ts} +0 -0
  449. /package/src/{section → dashboard}/section.module.css +0 -0
  450. /package/src/{section → dashboard}/section.module.css.d.ts +0 -0
  451. /package/src/{widget-header → dashboard}/widget-header.module.css +0 -0
  452. /package/src/{widget-header → dashboard}/widget-header.module.css.d.ts +0 -0
  453. /package/src/{widget-header → dashboard}/widget-header.tsx +0 -0
  454. /package/src/{connectivity/types.ts → provider/connectivity-types.ts} +0 -0
  455. /package/src/{nav/types.ts → shell/nav-types.ts} +0 -0
@@ -1,188 +1,19 @@
1
- const MONTHS = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']
2
- const SHORT_DAYS = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat']
3
-
4
1
  /**
5
- * What EVERY formatter here prints for input it cannot represent — `NaN`, `±Infinity`, an
6
- * `Invalid Date`. An em dash (U+2014), the same glyph the tooltip already uses for a missing value.
7
- *
8
- * The rule is one rule on purpose. `Intl.NumberFormat` renders `NaN` as the literal string `"NaN"`
9
- * and `Infinity` as `"∞"`, so a y axis whose domain collapsed (an empty series, a 0/0 rate, a log
10
- * scale reaching zero) painted its own arithmetic accident as a tick label and a tooltip read
11
- * `NaN%`. A chart may say it does not know; it may not print a number that is not one.
2
+ * Re-export shim. The implementation moved to `basalt-ui/format` (C5 consolidation, so
3
+ * `money`/`percent`/`duration`/`clock`/… and the chart formatters share one implementation
4
+ * instead of two). This file stays so every existing relative import inside `src/charts/**`
5
+ * (`../utils/format`) keeps resolving unchanged.
12
6
  */
13
- export const NON_FINITE = '\u2014'
14
-
15
- /** Axis-friendly short date: DD.MM. A non-finite or invalid date prints {@link NON_FINITE}. */
16
- export function fmtAxisDate(value: unknown): string {
17
- if (typeof value === 'number' && !Number.isFinite(value)) return NON_FINITE
18
- if (value instanceof Date) {
19
- if (Number.isNaN(value.getTime())) return NON_FINITE
20
- const dd = String(value.getDate()).padStart(2, '0')
21
- const mm = String(value.getMonth() + 1).padStart(2, '0')
22
- return `${dd}.${mm}`
23
- }
24
- const s = String(value ?? '')
25
- const match = s.match(/(\d{4})-(\d{2})-(\d{2})/)
26
- if (match) return `${match[3]}.${match[2]}`
27
- return s
28
- }
29
-
30
- /** Tooltip-friendly long date: "Mon Apr 21 2026". A non-finite or invalid date prints
31
- * {@link NON_FINITE}. */
32
- export function fmtTooltipDate(date: unknown): string {
33
- if (typeof date === 'number' && !Number.isFinite(date)) return NON_FINITE
34
- if (date instanceof Date) {
35
- if (Number.isNaN(date.getTime())) return NON_FINITE
36
- return `${SHORT_DAYS[date.getDay()]} ${MONTHS[date.getMonth()]} ${date.getDate()} ${date.getFullYear()}`
37
- }
38
- const s = String(date ?? '')
39
- const match = s.match(/(\d{4})-(\d{2})-(\d{2})/)
40
- if (!match) return s
41
- const d = new Date(Number(match[1]), Number(match[2]) - 1, Number(match[3]))
42
- return `${SHORT_DAYS[d.getDay()]} ${MONTHS[d.getMonth()]} ${d.getDate()} ${d.getFullYear()}`
43
- }
44
-
45
- /**
46
- * The number-format law.
47
- *
48
- * `format.ts` shipped two date formatters and nothing else, so every axis and every tooltip in
49
- * every demo and consumer hand-rolled its own `` `$${v}k` `` — a template literal that is not
50
- * locale-aware, not compact-aware, and different in each of the places it was written. These four
51
- * are `Intl.NumberFormat` with the arguments already decided, so a chart states WHAT a number is
52
- * rather than how to punctuate it.
53
- *
54
- * `locale` defaults to `undefined`, which is `Intl`'s own "use the runtime's locale" — the correct
55
- * default for an app whose reader is the person looking at it. Pass it explicitly (`'en-US'`) when
56
- * the output must be stable regardless of where it runs, which is what the tests here do.
57
- *
58
- * Every one of them returns {@link NON_FINITE} for input that is not a finite number — see its
59
- * JSDoc for why that is a law of this module rather than a per-call-site guard.
60
- */
61
-
62
- /** Options every formatter here accepts. */
63
- type LocaleOption = {
64
- /** BCP-47 tag. Default `undefined` — the runtime's own locale. */
65
- locale?: string
66
- }
67
-
68
- /**
69
- * `Intl.NumberFormat` construction is the expensive half of formatting and a chart formats one
70
- * label per tick per frame, so instances are memoized on their own arguments. The key is the
71
- * serialized options because that is exactly what identifies an instance.
72
- */
73
- const formatterCache = new Map<string, Intl.NumberFormat>()
74
-
75
- /** The compact `K` suffix, matched only where it follows the digits it qualifies — so a currency
76
- * prefix that happens to contain one (`HK$`) survives the lowercasing. */
77
- const COMPACT_K = /(\d\s?)K/
78
-
79
- function numberFormat(
80
- locale: string | undefined,
81
- options: Intl.NumberFormatOptions,
82
- ): Intl.NumberFormat {
83
- const key = `${locale ?? ''}|${JSON.stringify(options)}`
84
- const cached = formatterCache.get(key)
85
- if (cached !== undefined) return cached
86
- const created = new Intl.NumberFormat(locale, options)
87
- formatterCache.set(key, created)
88
- return created
89
- }
90
-
91
- export type FmtCompactOptions = LocaleOption & {
92
- /** Significant-ish fraction digits on the compacted number. Default 1 (`1.2k`, `3.4M`). */
93
- digits?: number
94
- }
95
-
96
- /**
97
- * Compact notation: `1.2k`, `3.4M`, `-820`. The axis formatter for any count whose range spans
98
- * more than three digits — a y axis reading `1,200,000` spends a third of the plot width on a
99
- * gutter. A non-finite value prints {@link NON_FINITE}.
100
- *
101
- * Note `Intl`'s compact notation is LOCALE text, not a suffix table: `en` gives `1.2K`, and this
102
- * lowercases the `K` to match the package's own numeral style while leaving every other locale's
103
- * word (`1,2 Mio.`) untouched. Only a `K` attached to the DIGITS is touched — `HK$1.2K` must keep
104
- * its currency prefix, which a bare `replace('K', 'k')` ate.
105
- */
106
- export function fmtCompact(value: number, options: FmtCompactOptions = {}): string {
107
- if (!Number.isFinite(value)) return NON_FINITE
108
- const { digits = 1, locale } = options
109
- return numberFormat(locale, {
110
- notation: 'compact',
111
- compactDisplay: 'short',
112
- maximumFractionDigits: digits,
113
- })
114
- .format(value)
115
- .replace(COMPACT_K, '$1k')
116
- }
117
-
118
- export type FmtPercentOptions = LocaleOption & {
119
- /** Fraction digits. Default 0 (`42%`). */
120
- digits?: number
121
- /**
122
- * What the incoming number MEANS. `'ratio'` (default) is `0.42 → 42%`; `'percent'` is
123
- * `42 → 42%`. Required as an explicit option rather than guessed from magnitude: a ratio of 1.2
124
- * and a percentage of 1.2 are both perfectly ordinary numbers, and a heuristic would silently
125
- * render one of them 100× wrong.
126
- */
127
- input?: 'ratio' | 'percent'
128
- }
129
-
130
- /** Percentages, from either a ratio or an already-scaled percentage. A non-finite value prints
131
- * {@link NON_FINITE} — a rate is the commonest source of a 0/0. */
132
- export function fmtPercent(value: number, options: FmtPercentOptions = {}): string {
133
- if (!Number.isFinite(value)) return NON_FINITE
134
- const { digits = 0, input = 'ratio', locale } = options
135
- return numberFormat(locale, {
136
- style: 'percent',
137
- minimumFractionDigits: digits,
138
- maximumFractionDigits: digits,
139
- }).format(input === 'percent' ? value / 100 : value)
140
- }
141
-
142
- export type FmtCurrencyOptions = LocaleOption & {
143
- /** ISO 4217 code — required. There is no default currency; a wrong symbol is worse than a
144
- * verbose call site. */
145
- currency: string
146
- /** Compact notation for the amount (`$1.2k`). Default false. */
147
- compact?: boolean
148
- /** Fraction digits. Default 0 compacted, else the currency's own default (`undefined`). */
149
- digits?: number
150
- }
151
-
152
- /** Money. `fmtCurrency(1234, { currency: 'USD' })` → `$1,234`. A non-finite amount prints
153
- * {@link NON_FINITE}. */
154
- export function fmtCurrency(value: number, options: FmtCurrencyOptions): string {
155
- if (!Number.isFinite(value)) return NON_FINITE
156
- const { currency, compact = false, digits, locale } = options
157
- const fractionDigits = digits ?? (compact ? 1 : 0)
158
- return numberFormat(locale, {
159
- style: 'currency',
160
- currency,
161
- ...(compact && { notation: 'compact', compactDisplay: 'short' }),
162
- minimumFractionDigits: compact ? 0 : fractionDigits,
163
- maximumFractionDigits: fractionDigits,
164
- })
165
- .format(value)
166
- .replace(COMPACT_K, '$1k')
167
- }
168
-
169
- /** Grouped integer: `12,480`. Rounds — an integer formatter that printed `12,480.4` would be
170
- * lying about its own name. A non-finite value prints {@link NON_FINITE}. */
171
- export function fmtInt(value: number, options: LocaleOption = {}): string {
172
- if (!Number.isFinite(value)) return NON_FINITE
173
- return numberFormat(options.locale, { maximumFractionDigits: 0 }).format(value)
174
- }
175
-
176
- /**
177
- * Every shipped formatter under one name, so a chart can be handed the whole set
178
- * (`format={formatters.compact}`) and a consumer has one import to reach for instead of four.
179
- * Same members as the named exports — this is a convenience, never a second implementation.
180
- */
181
- export const formatters = {
182
- compact: fmtCompact,
183
- percent: fmtPercent,
184
- currency: fmtCurrency,
185
- int: fmtInt,
186
- axisDate: fmtAxisDate,
187
- tooltipDate: fmtTooltipDate,
188
- } as const
7
+ export {
8
+ fmtAxisDate,
9
+ fmtTooltipDate,
10
+ fmtCompact,
11
+ type FmtCompactOptions,
12
+ fmtPercent,
13
+ type FmtPercentOptions,
14
+ fmtCurrency,
15
+ type FmtCurrencyOptions,
16
+ fmtInt,
17
+ formatters,
18
+ NON_FINITE,
19
+ } from '../../format'
@@ -0,0 +1,371 @@
1
+ /**
2
+ * `check-theme --audit-allows` — split out of `src/cli/index.ts` (C2) so the dispatcher file holds
3
+ * routing only. Imports its shared plumbing (config/project resolution) back from `./index`.
4
+ */
5
+ import { spawnSync } from 'node:child_process'
6
+ import { existsSync, readFileSync, unlinkSync, writeFileSync } from 'node:fs'
7
+ import { resolve } from 'node:path'
8
+
9
+ import {
10
+ checkSource,
11
+ findAllowAnnotations,
12
+ neutralizeAllowAnnotation,
13
+ unmatchedExemptPatterns,
14
+ } from '../guard'
15
+ import type { Finding, GuardConfig, GuardKind } from '../guard'
16
+ import type { BasaltConfig, ExemptRuleEntry } from './index'
17
+ import { exemptRulePaths, exemptRuleReason, resolveRoots } from './index'
18
+
19
+ // ── check-theme --audit-allows — every waiver, and whether it still suppresses anything ─────────
20
+
21
+ /** `relPath:line:kind` — a finding's identity for set arithmetic across two runs of the same file. */
22
+ function findingKey(f: Finding): string {
23
+ return `${f.relPath}:${f.line}:${f.kind}`
24
+ }
25
+
26
+ /** One annotation the guard cannot judge, queued for the oxlint half of the audit. */
27
+ type PluginProbe = {
28
+ /** Index into the report's line array, so the verdict lands where the placeholder sits. */
29
+ readonly slot: number
30
+ readonly rel: string
31
+ /** 1-based line of the annotation, as {@link findAllowAnnotations} reports it. */
32
+ readonly line: number
33
+ readonly ids: readonly string[]
34
+ readonly site: string
35
+ readonly suffix: string
36
+ }
37
+
38
+ /** One oxlint diagnostic, as `--format=json` renders it — only the fields the audit reads. */
39
+ type OxlintDiagnostic = {
40
+ code?: unknown
41
+ filename?: unknown
42
+ labels?: { span?: { line?: unknown } }[]
43
+ }
44
+
45
+ /** The nearest `node_modules/.bin/<name>` walking up from `dir`, else `name` if it is on PATH. */
46
+ function resolveToolBin(dir: string, name: string): string | null {
47
+ let current = dir
48
+ for (;;) {
49
+ const candidate = resolve(current, 'node_modules/.bin', name)
50
+ if (existsSync(candidate)) return candidate
51
+ const parent = resolve(current, '..')
52
+ if (parent === current) break
53
+ current = parent
54
+ }
55
+ const probe = spawnSync(name, ['--version'], { stdio: 'ignore', timeout: 10_000 })
56
+ return probe.error === undefined && probe.status === 0 ? name : null
57
+ }
58
+
59
+ /** `line:rule` counts for one file's diagnostics — a multiset, so two findings on a line differ. */
60
+ function diagnosticCounts(
61
+ diagnostics: readonly OxlintDiagnostic[],
62
+ filename: string,
63
+ ): Map<string, number> {
64
+ const counts = new Map<string, number>()
65
+ for (const d of diagnostics) {
66
+ if (d.filename !== filename) continue
67
+ const code = typeof d.code === 'string' ? d.code : ''
68
+ const rule = /^basalt\((.+)\)$/.exec(code)?.[1]
69
+ if (rule === undefined) continue
70
+ const line = d.labels?.[0]?.span?.line
71
+ const key = `${typeof line === 'number' ? line : 0}:${rule}`
72
+ counts.set(key, (counts.get(key) ?? 0) + 1)
73
+ }
74
+ return counts
75
+ }
76
+
77
+ /**
78
+ * The oxlint half of the audit: what each plugin-rule annotation still suppresses.
79
+ *
80
+ * The `check-theme` half proves a verdict by re-running `checkSource` with one occurrence
81
+ * neutralized. The `error`-severity design rules do not live there — they live in the oxlint
82
+ * plugin — so the audit declined to judge them and printed "not a check-theme kind". In a
83
+ * chart-heavy consumer that was 8 of 8 and 11 of 14: a CI gate that exited 0 having inspected
84
+ * nothing. The method generalizes exactly, one level over: neutralize the annotation, re-run
85
+ * oxlint over that file, and the findings that appear are what it covers.
86
+ *
87
+ * oxlint has no stdin mode, so a probe has to be a real file. It is written BESIDE the original —
88
+ * same directory, so the same `.oxlintrc.json`, the same `charts/` path segment, the same nearest
89
+ * `package.json` — and removed in a `finally`. Returns null when oxlint cannot be run at all, which
90
+ * is a "cannot judge", never a "dead".
91
+ */
92
+ function probePluginRules(
93
+ cwd: string,
94
+ guardCfg: GuardConfig,
95
+ probes: readonly PluginProbe[],
96
+ sources: ReadonlyMap<string, string>,
97
+ ): Map<number, string[]> | null {
98
+ if (probes.length === 0) return new Map()
99
+ const bin = resolveToolBin(cwd, 'oxlint')
100
+ if (bin === null) return null
101
+
102
+ const written: string[] = []
103
+ const probePaths = new Map<number, string>()
104
+ try {
105
+ for (const probe of probes) {
106
+ // Neutralized by the guard's own helper, so the oxlint probe and the checkSource probe can
107
+ // never differ by a substitution detail — a verdict that disagrees between the two halves of
108
+ // one audit is worse than no verdict.
109
+ const probed = neutralizeAllowAnnotation(sources.get(probe.rel) ?? '', probe.line, guardCfg)
110
+ const dot = probe.rel.lastIndexOf('.')
111
+ const rel =
112
+ dot === -1
113
+ ? `${probe.rel}.basalt-audit-${probe.slot}`
114
+ : `${probe.rel.slice(0, dot)}.basalt-audit-${probe.slot}${probe.rel.slice(dot)}`
115
+ const abs = resolve(cwd, rel)
116
+ if (existsSync(abs)) continue
117
+ writeFileSync(abs, probed, 'utf8')
118
+ written.push(abs)
119
+ probePaths.set(probe.slot, rel)
120
+ }
121
+ if (probePaths.size === 0) return null
122
+
123
+ const targets = [...new Set(probes.map((p) => p.rel)), ...probePaths.values()]
124
+ const run = spawnSync(bin, ['--format=json', '--no-error-on-unmatched-pattern', ...targets], {
125
+ cwd,
126
+ encoding: 'utf8',
127
+ timeout: 120_000,
128
+ stdio: ['ignore', 'pipe', 'ignore'],
129
+ })
130
+ if (run.error !== undefined || typeof run.stdout !== 'string') return null
131
+ let diagnostics: OxlintDiagnostic[]
132
+ try {
133
+ diagnostics =
134
+ (JSON.parse(run.stdout) as { diagnostics?: OxlintDiagnostic[] }).diagnostics ?? []
135
+ } catch {
136
+ return null
137
+ }
138
+
139
+ const verdicts = new Map<number, string[]>()
140
+ for (const probe of probes) {
141
+ const probeRel = probePaths.get(probe.slot)
142
+ if (probeRel === undefined) continue
143
+ const before = diagnosticCounts(diagnostics, probe.rel)
144
+ const after = diagnosticCounts(diagnostics, probeRel)
145
+ const revealed: string[] = []
146
+ for (const [key, count] of after) {
147
+ if (count <= (before.get(key) ?? 0)) continue
148
+ const [line = '', rule = ''] = key.split(':')
149
+ revealed.push(`${rule}@${line}`)
150
+ }
151
+ verdicts.set(probe.slot, revealed.toSorted())
152
+ }
153
+ return verdicts
154
+ } finally {
155
+ for (const abs of written) {
156
+ try {
157
+ unlinkSync(abs)
158
+ } catch {
159
+ // The probe is a temp file basalt owns; a failed unlink must not mask the audit's verdict.
160
+ }
161
+ }
162
+ }
163
+ }
164
+
165
+ /**
166
+ * `check-theme --audit-allows` — list every active waiver and prove, per waiver, whether it still
167
+ * suppresses anything.
168
+ *
169
+ * The accountability release shipped `theme-allow-unscoped`, which reports a waiver that names no
170
+ * rule — but a waiver that is perfectly well written and covers a finding that no longer exists is
171
+ * invisible to it, and to everything else. Two consumers found five of those by hand, by editing
172
+ * the token out and re-running the guard. This is that method, automated: for each annotation the
173
+ * file is re-checked with THAT occurrence neutralized, and the findings that appear are exactly
174
+ * what it suppresses. Nothing is inferred from the annotation's text, so the audit cannot disagree
175
+ * with the scan — it IS the scan, run twice.
176
+ *
177
+ * `basalt.exemptRules` entries are audited the same way, one pattern at a time. That also answers
178
+ * the question the key could never answer for itself: a pattern that matches no scanned file (a
179
+ * real relative path, say, where the matcher takes whole path SEGMENTS) suppresses nothing and is
180
+ * reported as dead rather than passing silently.
181
+ *
182
+ * Exit 1 when anything is dead, so it can be wired into CI as a gate. A waiver nobody re-checks is
183
+ * how a rule quietly stops applying to the file that needed the exception most.
184
+ */
185
+ export function auditAllows(
186
+ cwd: string,
187
+ cfg: BasaltConfig,
188
+ guardCfg: GuardConfig,
189
+ scanned: string[],
190
+ ): number {
191
+ const token = guardCfg.allowComment
192
+ const lines: string[] = [`\nbasalt-ui check-theme --audit-allows — ${cwd}\n`]
193
+ let dead = 0
194
+ let live = 0
195
+ let outOfReach = 0
196
+ let unaccountable = 0
197
+
198
+ // ── theme-allow annotations ─────────────────────────────────────────────────
199
+ const waiverLines: string[] = []
200
+ const pluginProbes: PluginProbe[] = []
201
+ const sources = new Map<string, string>()
202
+ for (const rel of scanned) sources.set(rel, readFileSync(resolve(cwd, rel), 'utf8'))
203
+
204
+ for (const rel of scanned) {
205
+ const text = sources.get(rel) ?? ''
206
+ if (!text.includes(token)) continue
207
+ const baseline = checkSource(text, rel, guardCfg)
208
+ const baselineKeys = new Set(
209
+ baseline.filter((f) => f.kind !== 'theme-allow-unscoped').map(findingKey),
210
+ )
211
+ const unscopedAt = new Map(
212
+ baseline.filter((f) => f.kind === 'theme-allow-unscoped').map((f) => [f.line, f.token]),
213
+ )
214
+ // Enumerated by the guard, never by a regex kept here: `findAllowAnnotations` shares its
215
+ // collector with `checkSource`, so the audit lists exactly the annotations the scan honours.
216
+ // The private mirror this replaced was already one comment shape behind.
217
+ for (const site of findAllowAnnotations(text, rel, guardCfg)) {
218
+ const revealed = checkSource(
219
+ neutralizeAllowAnnotation(text, site.line, guardCfg),
220
+ rel,
221
+ guardCfg,
222
+ )
223
+ .filter((f) => f.kind !== 'theme-allow-unscoped' && !baselineKeys.has(findingKey(f)))
224
+ .map((f) => `${f.kind}@${f.line}`)
225
+ const note = unscopedAt.get(site.line)
226
+ if (note !== undefined) unaccountable++
227
+ const label = ` ${rel}:${site.line}`.padEnd(52)
228
+ const suffix = note === undefined ? '' : ` [${note}]`
229
+ if (revealed.length > 0) {
230
+ live++
231
+ waiverLines.push(`${label} suppresses ${[...new Set(revealed)].join(', ')}${suffix}`)
232
+ continue
233
+ }
234
+ // Nothing in check-theme's reach moved. If the annotation names a PLUGIN rule, the oxlint
235
+ // half decides it — a placeholder holds the slot so the report stays in file/line order.
236
+ if (site.pluginRules.length > 0 && site.guardKinds.length === 0) {
237
+ pluginProbes.push({
238
+ slot: waiverLines.length,
239
+ rel,
240
+ line: site.line,
241
+ ids: site.pluginRules,
242
+ site: label,
243
+ suffix,
244
+ })
245
+ waiverLines.push('')
246
+ continue
247
+ }
248
+ if (site.guardKinds.length === 0 && site.unknownRules.length > 0) {
249
+ outOfReach++
250
+ waiverLines.push(
251
+ `${label} scoped to ${site.unknownRules.join(', ')} — names no guard kind and no ` +
252
+ 'plugin rule, so this audit cannot judge it (most likely a typo, which ' +
253
+ `theme-allow-unscoped reports)${suffix}`,
254
+ )
255
+ continue
256
+ }
257
+ dead++
258
+ waiverLines.push(`${label} SUPPRESSES NOTHING — dead, delete it${suffix}`)
259
+ }
260
+ }
261
+
262
+ // ── the oxlint half — plugin-rule annotations, judged the same way ──────────
263
+ const pluginVerdicts = probePluginRules(cwd, guardCfg, pluginProbes, sources)
264
+ for (const probe of pluginProbes) {
265
+ const revealed = pluginVerdicts?.get(probe.slot)
266
+ if (revealed === undefined) {
267
+ outOfReach++
268
+ waiverLines[probe.slot] =
269
+ `${probe.site} scoped to ${probe.ids.join(', ')} — an oxlint plugin rule, and oxlint ` +
270
+ `could not be run here, so this audit cannot judge it${probe.suffix}`
271
+ continue
272
+ }
273
+ if (revealed.length > 0) {
274
+ live++
275
+ waiverLines[probe.slot] =
276
+ `${probe.site} suppresses ${revealed.join(', ')} (oxlint)${probe.suffix}`
277
+ continue
278
+ }
279
+ dead++
280
+ waiverLines[probe.slot] =
281
+ `${probe.site} SUPPRESSES NOTHING — dead, delete it (proved by re-running oxlint over the ` +
282
+ `file with this annotation neutralized)${probe.suffix}`
283
+ }
284
+
285
+ lines.push(`${token} annotations (${waiverLines.length}):`)
286
+ lines.push(...(waiverLines.length === 0 ? [' (none)'] : waiverLines))
287
+
288
+ // ── basalt.exemptRules entries ──────────────────────────────────────────────
289
+ const exemptLines: string[] = []
290
+ let exemptCount = 0
291
+ const declared = Object.entries(cfg.exemptRules ?? {}) as [GuardKind, ExemptRuleEntry][]
292
+ if (declared.length > 0) {
293
+ const baselineKeys = new Set<string>()
294
+ for (const rel of scanned) {
295
+ for (const f of checkSource(sources.get(rel) ?? '', rel, guardCfg))
296
+ baselineKeys.add(findingKey(f))
297
+ }
298
+ for (const [kind, entry] of declared) {
299
+ const reason = exemptRuleReason(entry)
300
+ for (const pattern of exemptRulePaths(entry)) {
301
+ exemptCount++
302
+ const probeCfg: GuardConfig = {
303
+ ...guardCfg,
304
+ exemptRules: {
305
+ ...guardCfg.exemptRules,
306
+ [kind]: exemptRulePaths(entry).filter((p) => p !== pattern),
307
+ },
308
+ }
309
+ const revealed = new Set<string>()
310
+ for (const rel of scanned) {
311
+ for (const f of checkSource(sources.get(rel) ?? '', rel, probeCfg)) {
312
+ if (!baselineKeys.has(findingKey(f))) revealed.add(f.relPath)
313
+ }
314
+ }
315
+ const site = ` ${kind}: "${pattern}"`.padEnd(52)
316
+ const why = reason === null ? ' [no reason recorded]' : ` — ${reason}`
317
+ if (revealed.size === 0) {
318
+ dead++
319
+ const unmatched = unmatchedExemptPatterns(guardCfg, scanned).find(
320
+ (u) => u.kind === kind && u.pattern === pattern,
321
+ )
322
+ exemptLines.push(
323
+ `${site} SUPPRESSES NOTHING — ` +
324
+ (unmatched === undefined
325
+ ? 'the files it matches have no such finding; delete it'
326
+ : unmatched.reason === 'unknown-kind'
327
+ ? 'it names no guard kind (a typo, or a renamed kind)'
328
+ : 'it matches no scanned file at all') +
329
+ why,
330
+ )
331
+ } else {
332
+ live++
333
+ exemptLines.push(
334
+ `${site} suppresses findings in ${[...revealed].toSorted().join(', ')}${why}`,
335
+ )
336
+ }
337
+ }
338
+ }
339
+ }
340
+ lines.push(`\nbasalt.exemptRules entries (${exemptCount}):`)
341
+ lines.push(...(exemptLines.length === 0 ? [' (none)'] : exemptLines))
342
+
343
+ lines.push(
344
+ `\n${live} live, ${dead} dead, ${outOfReach} unjudgeable, ${unaccountable} ` +
345
+ 'unaccountable (reported as theme-allow-unscoped by a normal run).',
346
+ )
347
+ // The audit reads exactly what `check-theme` reads, and that is `scannableFiles` — which is
348
+ // WIDER than `basalt.roots`: it also takes each root's sibling `index.html` and `public/` tree
349
+ // (see appShellFiles) and anything named in `basalt.include`. Two consumers reported a live
350
+ // `public/site.webmanifest` waiver under a scope line saying files outside `roots` are not
351
+ // audited; the line was wrong, not the scan. Saying the scope out loud is the difference between
352
+ // "0 dead" and "0 dead, over these files" — so it has to name every class actually reached.
353
+ const includes = cfg.include ?? []
354
+ lines.push(
355
+ `Scope: the ${scanned.length} file(s) check-theme scans — everything under basalt.roots ` +
356
+ `(${resolveRoots(cfg).join(', ')}), plus each root's sibling index.html and public/ tree, ` +
357
+ (includes.length === 0
358
+ ? 'plus anything named in basalt.include (none). '
359
+ : `plus basalt.include (${includes.join(', ')}). `) +
360
+ 'A waiver in a file none of those reach is not audited — widen roots, or name it in ' +
361
+ 'basalt.include, or accept that nothing polices that file.',
362
+ )
363
+ if (dead > 0) {
364
+ lines.push(
365
+ 'A dead waiver is not harmless: it is an exception nobody re-checked, and it will silently ' +
366
+ 'cover the next real finding on that line.',
367
+ )
368
+ }
369
+ console.log(lines.join('\n'))
370
+ return dead > 0 ? 1 : 0
371
+ }