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.
- package/AGENTS.md +11 -18
- package/CHANGELOG.md +13 -0
- package/MIGRATING.md +151 -7
- package/README.md +23 -66
- package/agent/rules/basalt-batteries.md +59 -182
- package/agent/rules/basalt-charts.md +56 -75
- package/agent/rules/basalt-controls.md +38 -69
- package/agent/rules/basalt-mantine.md +58 -103
- package/agent/rules/basalt-state.md +42 -60
- package/agent/rules/basalt-tokens.md +46 -67
- package/agent/skills/basalt-app/SKILL.md +2 -0
- package/agent/templates/CLAUDE-block.md.tpl +14 -14
- package/configs/oxlint-plugin.js +70 -167
- package/configs/oxlint-plugin.test.ts +20 -186
- package/configs/oxlint.json +0 -1
- package/dist/agent/ai-sdk-transport.d.ts.map +1 -1
- package/dist/agent/ai-sdk-transport.js +7 -4
- package/dist/agent/coalesce.js +1 -1
- package/dist/agent/merge.js +1 -1
- package/dist/agent/transport.d.ts +13 -2
- package/dist/agent/transport.d.ts.map +1 -1
- package/dist/agent/use-agent-thread-runs.d.ts +12 -1
- package/dist/agent/use-agent-thread-runs.d.ts.map +1 -1
- package/dist/agent/use-agent-thread-runs.js +20 -6
- package/dist/agent-chat/index.d.ts +1 -1
- package/dist/agent-chat/index.d.ts.map +1 -1
- package/dist/agent-chat/relative-time.js +1 -1
- package/dist/agent-chat/thread-detail-panel.js +1 -1
- package/dist/agent-chat/thread-feed-row.d.ts +32 -7
- package/dist/agent-chat/thread-feed-row.d.ts.map +1 -1
- package/dist/agent-chat/thread-feed-row.js +121 -86
- package/dist/agent-chat/thread-feed.d.ts +11 -1
- package/dist/agent-chat/thread-feed.d.ts.map +1 -1
- package/dist/agent-chat/thread-feed.js +10 -6
- package/dist/agent-chat/thread-message.js +1 -1
- package/dist/agent-chat/virtualize.d.ts +39 -0
- package/dist/agent-chat/virtualize.d.ts.map +1 -1
- package/dist/agent-chat/virtualize.js +15 -0
- package/dist/charts/index.d.ts +3 -3
- package/dist/charts/index.d.ts.map +1 -1
- package/dist/charts/index.js +8 -4
- package/dist/charts/kinds/BandStrip.js +1 -1
- package/dist/charts/kinds/Bars.d.ts +1 -3
- package/dist/charts/kinds/Bars.d.ts.map +1 -1
- package/dist/charts/kinds/Bars.js +1 -1
- package/dist/charts/kinds/DualPanel.d.ts +3 -1
- package/dist/charts/kinds/DualPanel.d.ts.map +1 -1
- package/dist/charts/kinds/DualPanel.js +1 -1
- package/dist/charts/kinds/StackedArea.js +1 -1
- package/dist/charts/kinds/ZonedLine.d.ts +1 -3
- package/dist/charts/kinds/ZonedLine.d.ts.map +1 -1
- package/dist/charts/primitives/Axes.d.ts +21 -2
- package/dist/charts/primitives/Axes.d.ts.map +1 -1
- package/dist/charts/primitives/Axes.js +34 -2
- package/dist/charts/primitives/ChartCard.d.ts +20 -4
- package/dist/charts/primitives/ChartCard.d.ts.map +1 -1
- package/dist/charts/primitives/ChartCard.js +62 -33
- package/dist/charts/primitives/ChartFrame.d.ts.map +1 -1
- package/dist/charts/primitives/ChartFrame.js +8 -1
- package/dist/charts/primitives/ChartPending.d.ts +12 -6
- package/dist/charts/primitives/ChartPending.d.ts.map +1 -1
- package/dist/charts/primitives/ChartPending.js +1 -1
- package/dist/charts/utils/format.d.ts +5 -92
- package/dist/charts/utils/format.d.ts.map +1 -1
- package/dist/charts/utils/format.js +10 -80
- package/dist/cli/audit-allows.d.ts +24 -0
- package/dist/cli/audit-allows.d.ts.map +1 -0
- package/dist/cli/audit-allows.js +240 -0
- package/dist/cli/check-theme.d.ts +10 -0
- package/dist/cli/check-theme.d.ts.map +1 -0
- package/dist/cli/check-theme.js +139 -0
- package/dist/cli/doctor.d.ts +47 -0
- package/dist/cli/doctor.d.ts.map +1 -0
- package/dist/cli/doctor.js +197 -0
- package/dist/cli/guard-hook.d.ts +9 -0
- package/dist/cli/guard-hook.d.ts.map +1 -0
- package/dist/cli/guard-hook.js +93 -0
- package/dist/cli/help.d.ts +9 -0
- package/dist/cli/help.d.ts.map +1 -0
- package/dist/cli/help.js +53 -0
- package/dist/cli/index.d.ts +360 -270
- package/dist/cli/index.d.ts.map +1 -1
- package/dist/cli/index.js +85 -1901
- package/dist/cli/init.d.ts +4 -0
- package/dist/cli/init.d.ts.map +1 -0
- package/dist/cli/init.js +165 -0
- package/dist/cli/sync.d.ts +43 -0
- package/dist/cli/sync.d.ts.map +1 -0
- package/dist/cli/sync.js +206 -0
- package/dist/cli/tokens-css.d.ts +51 -0
- package/dist/cli/tokens-css.d.ts.map +1 -0
- package/dist/cli/tokens-css.js +165 -0
- package/dist/commands/define-commands.js +1 -1
- package/dist/commands/define-overlays.js +1 -1
- package/dist/commands/index.d.ts +2 -1
- package/dist/commands/index.d.ts.map +1 -1
- package/dist/commands/index.js +6 -0
- package/dist/commands/overlays-mount.d.ts +7 -9
- package/dist/commands/overlays-mount.d.ts.map +1 -1
- package/dist/commands/shell-bridge.d.ts +57 -0
- package/dist/commands/shell-bridge.d.ts.map +1 -0
- package/dist/commands/shell-bridge.js +32 -0
- package/dist/common/errors.d.ts +35 -0
- package/dist/common/errors.d.ts.map +1 -1
- package/dist/common/errors.js +29 -1
- package/dist/common/index.d.ts +2 -0
- package/dist/common/index.d.ts.map +1 -1
- package/dist/common/index.js +6 -0
- package/dist/common/is-dev.d.ts.map +1 -0
- package/dist/{motion/index.d.ts → common/motion.d.ts} +1 -1
- package/dist/common/motion.d.ts.map +1 -0
- package/dist/common/props.d.ts +1 -1
- package/dist/common/props.d.ts.map +1 -1
- package/dist/common/validate.js +1 -1
- package/dist/content/markdown.d.ts +3 -4
- package/dist/content/markdown.d.ts.map +1 -1
- package/dist/content/markdown.js +1 -4
- package/dist/controls/actions.d.ts +1 -1
- package/dist/controls/actions.d.ts.map +1 -1
- package/dist/controls/panel-row.js +1 -1
- package/dist/controls/range-filter.js +1 -1
- package/dist/dashboard/delta-badge.d.ts +18 -10
- package/dist/dashboard/delta-badge.d.ts.map +1 -1
- package/dist/dashboard/delta-badge.js +33 -1
- package/dist/dashboard/empty-state.d.ts +1 -4
- package/dist/dashboard/empty-state.d.ts.map +1 -1
- package/dist/dashboard/empty-state.js +1 -2
- package/dist/dashboard/index.d.ts +4 -2
- package/dist/dashboard/index.d.ts.map +1 -1
- package/dist/dashboard/index.js +7 -1
- package/dist/dashboard/query-state.d.ts +4 -13
- package/dist/dashboard/query-state.d.ts.map +1 -1
- package/dist/dashboard/query-state.js +4 -7
- package/dist/{section → dashboard}/section.d.ts +2 -2
- package/dist/{section → dashboard}/section.d.ts.map +1 -1
- package/dist/{section → dashboard}/section.js +2 -2
- package/dist/dashboard/settings-section.d.ts +1 -1
- package/dist/dashboard/settings-section.d.ts.map +1 -1
- package/dist/dashboard/settings-section.js +1 -1
- package/dist/dashboard/stat-card.d.ts +1 -1
- package/dist/dashboard/stat-card.d.ts.map +1 -1
- package/dist/dashboard/stat-card.js +1 -1
- package/dist/dashboard/widget-header.d.ts.map +1 -0
- package/dist/data/data-table.js +2 -2
- package/dist/data/table.d.ts +1 -1
- package/dist/data/table.d.ts.map +1 -1
- package/dist/data/table.js +2 -16
- package/dist/data/virtual.d.ts +3 -3
- package/dist/dev-dock.d.ts +28 -0
- package/dist/dev-dock.d.ts.map +1 -0
- package/dist/dev-dock.js +85 -0
- package/dist/format/date.d.ts +62 -0
- package/dist/format/date.d.ts.map +1 -0
- package/dist/format/date.js +106 -0
- package/dist/format/index.d.ts +39 -0
- package/dist/format/index.d.ts.map +1 -0
- package/dist/format/index.js +54 -0
- package/dist/format/measure.d.ts +15 -0
- package/dist/format/measure.d.ts.map +1 -0
- package/dist/format/measure.js +16 -0
- package/dist/format/number.d.ts +69 -0
- package/dist/format/number.d.ts.map +1 -0
- package/dist/format/number.js +62 -0
- package/dist/format/shared.d.ts +27 -0
- package/dist/format/shared.d.ts.map +1 -0
- package/dist/format/shared.js +29 -0
- package/dist/forms/field.d.ts +4 -20
- package/dist/forms/field.d.ts.map +1 -1
- package/dist/forms/field.js +0 -4
- package/dist/forms/form-layout.d.ts +1 -1
- package/dist/forms/form-layout.d.ts.map +1 -1
- package/dist/forms/form-layout.js +1 -1
- package/dist/forms/index.d.ts +1 -1
- package/dist/forms/index.d.ts.map +1 -1
- package/dist/forms/index.js +1 -2
- package/dist/forms/use-form-submit.js +1 -1
- package/dist/guard/index.d.ts +2 -9
- package/dist/guard/index.d.ts.map +1 -1
- package/dist/guard/index.js +16 -26
- package/dist/guard/types.d.ts +8 -10
- package/dist/guard/types.d.ts.map +1 -1
- package/dist/index.d.ts +16 -11
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +23 -29
- package/dist/notifications/index.d.ts +5 -5
- package/dist/notifications/index.d.ts.map +1 -1
- package/dist/notifications/index.js +0 -2
- package/dist/notifications/mount-guard.d.ts.map +1 -1
- package/dist/notifications/mount-guard.js +2 -2
- package/dist/provider/connectivity-indicator.d.ts.map +1 -0
- package/dist/{connectivity → provider}/connectivity-provider.d.ts +1 -1
- package/dist/provider/connectivity-provider.d.ts.map +1 -0
- package/dist/{connectivity/types.d.ts → provider/connectivity-types.d.ts} +1 -1
- package/dist/provider/connectivity-types.d.ts.map +1 -0
- package/dist/provider/error-fallback.module.css +21 -0
- package/dist/provider/error-fallback.module.css.d.ts +9 -0
- package/dist/provider/index.d.ts +20 -36
- package/dist/provider/index.d.ts.map +1 -1
- package/dist/provider/index.js +42 -32
- package/dist/provider/lab-theme.js +1 -1
- package/dist/provider/use-connectivity.d.ts +3 -0
- package/dist/provider/use-connectivity.d.ts.map +1 -0
- package/dist/query-client.d.ts +63 -0
- package/dist/query-client.d.ts.map +1 -0
- package/dist/query-client.js +33 -0
- package/dist/{query/devtools.d.ts → query-devtools.d.ts} +2 -2
- package/dist/query-devtools.d.ts.map +1 -0
- package/dist/register.d.ts +17 -3
- package/dist/register.d.ts.map +1 -1
- package/dist/router-tanstack/index.d.ts +4 -7
- package/dist/router-tanstack/index.d.ts.map +1 -1
- package/dist/router-tanstack/index.js +1 -9
- package/dist/router-tanstack/nav.d.ts +1 -1
- package/dist/router-tanstack/nav.d.ts.map +1 -1
- package/dist/router-tanstack/search-store.d.ts +3 -6
- package/dist/router-tanstack/search-store.d.ts.map +1 -1
- package/dist/router-tanstack/use-nav.d.ts +1 -1
- package/dist/router-tanstack/use-nav.d.ts.map +1 -1
- package/dist/shell/app-breadcrumbs.d.ts +1 -1
- package/dist/shell/app-breadcrumbs.d.ts.map +1 -1
- package/dist/shell/app-main.module.css +11 -0
- package/dist/shell/app-mobile-nav.d.ts +1 -1
- package/dist/shell/app-mobile-nav.d.ts.map +1 -1
- package/dist/shell/app-mobile-nav.js +24 -2
- package/dist/shell/app-sidebar.d.ts +1 -1
- package/dist/shell/app-sidebar.d.ts.map +1 -1
- package/dist/shell/app-sidebar.js +31 -2
- package/dist/shell/app-sidebar.module.css +10 -0
- package/dist/shell/index.d.ts +10 -4
- package/dist/shell/index.d.ts.map +1 -1
- package/dist/shell/index.js +13 -17
- package/dist/shell/mobile-nav-model.d.ts +1 -1
- package/dist/shell/mobile-nav-model.d.ts.map +1 -1
- package/dist/{nav/types.d.ts → shell/nav-types.d.ts} +1 -1
- package/dist/shell/nav-types.d.ts.map +1 -0
- package/dist/shell/page-aside.d.ts.map +1 -1
- package/dist/shell/page-aside.js +2 -29
- package/dist/shell/page-title.d.ts +14 -0
- package/dist/shell/page-title.d.ts.map +1 -0
- package/dist/shell/page-title.js +25 -0
- package/dist/shell/page-title.module.css +33 -0
- package/dist/shell/page-title.module.css.d.ts +11 -0
- package/dist/shell/sidebar-block-model.d.ts +1 -1
- package/dist/shell/sidebar-block-model.d.ts.map +1 -1
- package/dist/shell/sidebar-blocks.d.ts +1 -1
- package/dist/shell/sidebar-blocks.d.ts.map +1 -1
- package/dist/shell/use-breakpoint.d.ts +11 -0
- package/dist/shell/use-breakpoint.d.ts.map +1 -0
- package/dist/shell/use-breakpoint.js +33 -0
- package/dist/state/fields.d.ts +3 -19
- package/dist/state/fields.d.ts.map +1 -1
- package/dist/state/fields.js +6 -16
- package/dist/styles.css +36 -0
- package/dist/surfaces.d.ts +18 -41
- package/dist/surfaces.d.ts.map +1 -1
- package/dist/surfaces.js +46 -59
- package/dist/theme/index.d.ts.map +1 -1
- package/dist/{theme-toggle/index.d.ts → theme/theme-toggle.d.ts} +1 -1
- package/dist/theme/theme-toggle.d.ts.map +1 -0
- package/dist/{theme-toggle/index.js → theme/theme-toggle.js} +1 -1
- package/dist/tokens/index.d.ts +1 -0
- package/dist/tokens/index.d.ts.map +1 -1
- package/dist/tokens/index.js +10 -0
- package/dist/tokens/palette.d.ts +12 -0
- package/dist/tokens/palette.d.ts.map +1 -1
- package/dist/tokens/palette.js +13 -1
- package/dist/tokens.css +1 -0
- package/llms.txt +9 -24
- package/package.json +12 -16
- package/src/agent/ai-sdk-transport.ts +11 -4
- package/src/agent/coalesce.ts +1 -1
- package/src/agent/merge.ts +1 -1
- package/src/agent/transport.ts +15 -2
- package/src/agent/use-agent-thread-runs.ts +65 -15
- package/src/agent-chat/index.ts +6 -4
- package/src/agent-chat/relative-time.ts +1 -1
- package/src/agent-chat/thread-detail-panel.tsx +1 -1
- package/src/agent-chat/thread-feed-row.tsx +105 -30
- package/src/agent-chat/thread-feed.tsx +51 -25
- package/src/agent-chat/thread-message.tsx +1 -1
- package/src/agent-chat/virtualize.ts +48 -0
- package/src/charts/index.ts +7 -10
- package/src/charts/kinds/BandStrip.tsx +1 -1
- package/src/charts/kinds/Bars.tsx +2 -5
- package/src/charts/kinds/DualPanel.tsx +4 -3
- package/src/charts/kinds/StackedArea.tsx +1 -1
- package/src/charts/kinds/ZonedLine.tsx +1 -4
- package/src/charts/primitives/Axes.tsx +49 -0
- package/src/charts/primitives/ChartCard.tsx +49 -6
- package/src/charts/primitives/ChartFrame.tsx +5 -1
- package/src/charts/primitives/ChartPending.tsx +14 -8
- package/src/charts/utils/format.ts +17 -186
- package/src/cli/audit-allows.ts +371 -0
- package/src/cli/check-theme.ts +197 -0
- package/src/cli/doctor.ts +331 -0
- package/src/cli/guard-hook.ts +115 -0
- package/src/cli/help.ts +101 -0
- package/src/cli/index.ts +269 -3439
- package/src/cli/init.ts +247 -0
- package/src/cli/sync.ts +324 -0
- package/src/cli/tokens-css.ts +252 -0
- package/src/commands/define-commands.ts +1 -1
- package/src/commands/define-overlays.ts +1 -1
- package/src/commands/index.ts +10 -1
- package/src/commands/overlays-mount.tsx +8 -10
- package/src/commands/shell-bridge.ts +86 -0
- package/src/common/errors.ts +70 -0
- package/src/common/index.ts +2 -0
- package/src/common/props.ts +1 -1
- package/src/common/validate.ts +1 -1
- package/src/content/markdown.tsx +4 -8
- package/src/controls/actions.tsx +1 -1
- package/src/controls/panel-row.tsx +1 -1
- package/src/controls/range-filter.tsx +1 -1
- package/src/dashboard/delta-badge.tsx +74 -7
- package/src/dashboard/empty-state.tsx +2 -6
- package/src/dashboard/index.ts +11 -2
- package/src/dashboard/query-state.tsx +5 -18
- package/src/{section → dashboard}/section.tsx +4 -4
- package/src/dashboard/settings-section.tsx +2 -2
- package/src/dashboard/stat-card.tsx +2 -2
- package/src/data/data-table.tsx +3 -3
- package/src/data/table.ts +6 -25
- package/src/data/virtual-list.tsx +1 -1
- package/src/data/virtual.ts +3 -3
- package/src/dev-dock.tsx +159 -0
- package/src/format/date.ts +184 -0
- package/src/format/index.ts +69 -0
- package/src/format/measure.ts +31 -0
- package/src/format/number.ts +129 -0
- package/src/format/shared.ts +63 -0
- package/src/forms/field.ts +4 -24
- package/src/forms/form-layout.tsx +2 -2
- package/src/forms/index.ts +1 -2
- package/src/forms/use-form-submit.ts +1 -1
- package/src/guard/index.ts +30 -56
- package/src/guard/types.ts +7 -10
- package/src/index.ts +38 -29
- package/src/notifications/index.ts +5 -7
- package/src/notifications/mount-guard.ts +7 -9
- package/src/{connectivity → provider}/connectivity-indicator.tsx +1 -1
- package/src/{connectivity → provider}/connectivity-provider.tsx +1 -1
- package/src/provider/error-fallback.module.css +21 -0
- package/src/provider/error-fallback.module.css.d.ts +9 -0
- package/src/provider/index.tsx +71 -71
- package/src/provider/lab-theme.ts +1 -1
- package/src/{connectivity → provider}/use-connectivity.ts +3 -3
- package/src/query-client.ts +100 -0
- package/src/{query/devtools.tsx → query-devtools.tsx} +5 -4
- package/src/register.ts +20 -4
- package/src/router-tanstack/index.ts +4 -15
- package/src/router-tanstack/nav.ts +1 -1
- package/src/router-tanstack/search-store.ts +3 -4
- package/src/router-tanstack/use-nav.ts +1 -1
- package/src/shell/app-breadcrumbs.tsx +1 -1
- package/src/shell/app-main.module.css +11 -0
- package/src/shell/app-mobile-nav.tsx +28 -1
- package/src/shell/app-sidebar.module.css +10 -0
- package/src/shell/app-sidebar.tsx +31 -2
- package/src/shell/index.tsx +23 -28
- package/src/shell/mobile-nav-model.ts +1 -1
- package/src/shell/page-aside.tsx +2 -52
- package/src/shell/page-title.module.css +33 -0
- package/src/shell/page-title.module.css.d.ts +11 -0
- package/src/shell/page-title.tsx +57 -0
- package/src/shell/sidebar-block-model.ts +1 -1
- package/src/shell/sidebar-blocks.tsx +1 -1
- package/src/shell/use-breakpoint.ts +73 -0
- package/src/state/fields.ts +7 -43
- package/src/styles.css +36 -0
- package/src/surfaces.ts +45 -60
- package/src/theme/index.ts +7 -0
- package/src/{theme-toggle/index.tsx → theme/theme-toggle.tsx} +1 -1
- package/src/tokens/index.ts +10 -0
- package/src/tokens/palette.ts +12 -0
- package/dist/connectivity/connectivity-indicator.d.ts.map +0 -1
- package/dist/connectivity/connectivity-provider.d.ts.map +0 -1
- package/dist/connectivity/index.d.ts +0 -14
- package/dist/connectivity/index.d.ts.map +0 -1
- package/dist/connectivity/index.js +0 -8
- package/dist/connectivity/types.d.ts.map +0 -1
- package/dist/connectivity/use-connectivity.d.ts +0 -3
- package/dist/connectivity/use-connectivity.d.ts.map +0 -1
- package/dist/data/index.d.ts +0 -38
- package/dist/data/index.d.ts.map +0 -1
- package/dist/data/index.js +0 -24
- package/dist/motion/index.d.ts.map +0 -1
- package/dist/nav/types.d.ts.map +0 -1
- package/dist/notifications/overlay.d.ts +0 -20
- package/dist/notifications/overlay.d.ts.map +0 -1
- package/dist/notifications/overlay.js +0 -14
- package/dist/query/devtools.d.ts.map +0 -1
- package/dist/query/error-message.d.ts +0 -41
- package/dist/query/error-message.d.ts.map +0 -1
- package/dist/query/error-message.js +0 -30
- package/dist/query/index.d.ts +0 -47
- package/dist/query/index.d.ts.map +0 -1
- package/dist/query/index.js +0 -56
- package/dist/router-tanstack/field.d.ts +0 -14
- package/dist/router-tanstack/field.d.ts.map +0 -1
- package/dist/router-tanstack/field.js +0 -4
- package/dist/router-tanstack/multi-search-param-store.d.ts +0 -43
- package/dist/router-tanstack/multi-search-param-store.d.ts.map +0 -1
- package/dist/router-tanstack/multi-search-param-store.js +0 -30
- package/dist/router-tanstack/search-param-store.d.ts +0 -42
- package/dist/router-tanstack/search-param-store.d.ts.map +0 -1
- package/dist/router-tanstack/search-param-store.js +0 -25
- package/dist/section/index.d.ts +0 -2
- package/dist/section/index.d.ts.map +0 -1
- package/dist/section/index.js +0 -4
- package/dist/theme-toggle/index.d.ts.map +0 -1
- package/dist/utils/is-dev.d.ts.map +0 -1
- package/dist/widget-header/delta-badge.d.ts +0 -19
- package/dist/widget-header/delta-badge.d.ts.map +0 -1
- package/dist/widget-header/delta-badge.js +0 -36
- package/dist/widget-header/index.d.ts +0 -3
- package/dist/widget-header/index.d.ts.map +0 -1
- package/dist/widget-header/index.js +0 -8
- package/dist/widget-header/widget-header.d.ts.map +0 -1
- package/src/connectivity/index.ts +0 -19
- package/src/data/index.ts +0 -69
- package/src/notifications/overlay.tsx +0 -66
- package/src/query/error-message.ts +0 -70
- package/src/query/index.ts +0 -97
- package/src/router-tanstack/field.ts +0 -36
- package/src/router-tanstack/multi-search-param-store.ts +0 -106
- package/src/router-tanstack/search-param-store.ts +0 -100
- package/src/section/index.ts +0 -1
- package/src/widget-header/delta-badge.tsx +0 -77
- package/src/widget-header/index.ts +0 -10
- /package/dist/{utils → common}/is-dev.d.ts +0 -0
- /package/dist/{utils → common}/is-dev.js +0 -0
- /package/dist/{motion/index.js → common/motion.js} +0 -0
- /package/dist/{section → dashboard}/section.module.css +0 -0
- /package/dist/{section → dashboard}/section.module.css.d.ts +0 -0
- /package/dist/{widget-header → dashboard}/widget-header.d.ts +0 -0
- /package/dist/{widget-header → dashboard}/widget-header.js +0 -0
- /package/dist/{widget-header → dashboard}/widget-header.module.css +0 -0
- /package/dist/{widget-header → dashboard}/widget-header.module.css.d.ts +0 -0
- /package/dist/{connectivity → provider}/connectivity-indicator.d.ts +0 -0
- /package/dist/{connectivity → provider}/connectivity-indicator.js +0 -0
- /package/dist/{connectivity → provider}/connectivity-provider.js +0 -0
- /package/dist/{connectivity/types.js → provider/connectivity-types.js} +0 -0
- /package/dist/{connectivity → provider}/use-connectivity.js +0 -0
- /package/dist/{query/devtools.js → query-devtools.js} +0 -0
- /package/dist/{nav/types.js → shell/nav-types.js} +0 -0
- /package/src/{utils → common}/is-dev.ts +0 -0
- /package/src/{motion/index.ts → common/motion.ts} +0 -0
- /package/src/{section → dashboard}/section.module.css +0 -0
- /package/src/{section → dashboard}/section.module.css.d.ts +0 -0
- /package/src/{widget-header → dashboard}/widget-header.module.css +0 -0
- /package/src/{widget-header → dashboard}/widget-header.module.css.d.ts +0 -0
- /package/src/{widget-header → dashboard}/widget-header.tsx +0 -0
- /package/src/{connectivity/types.ts → provider/connectivity-types.ts} +0 -0
- /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
|
-
*
|
|
6
|
-
* `
|
|
7
|
-
*
|
|
8
|
-
*
|
|
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
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
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
|
+
}
|