basalt-ui 1.24.0 → 1.26.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 +8 -6
- package/CHANGELOG.md +25 -0
- package/MIGRATING.md +547 -21
- package/README.md +46 -33
- package/agent/rules/basalt-batteries.md +205 -0
- package/agent/rules/basalt-charts.md +125 -555
- package/agent/rules/basalt-controls.md +155 -0
- package/agent/rules/basalt-mantine.md +146 -439
- package/agent/rules/basalt-state.md +113 -96
- package/agent/rules/basalt-tokens.md +126 -288
- package/agent/skills/basalt-app/SKILL.md +70 -195
- package/agent/skills/basalt-charts/SKILL.md +54 -234
- package/agent/skills/basalt-design/SKILL.md +56 -248
- package/agent/templates/CLAUDE-block.md.tpl +31 -40
- package/agent/templates/DESIGN.md.tpl +22 -51
- package/configs/oxlint-plugin.js +1275 -31
- package/configs/oxlint-plugin.test.ts +947 -11
- package/configs/oxlint.json +11 -3
- package/dist/charts/cursor/resolve.d.ts +18 -0
- package/dist/charts/cursor/resolve.d.ts.map +1 -1
- package/dist/charts/cursor/resolve.js +15 -6
- package/dist/charts/cursor/scope.d.ts +6 -3
- package/dist/charts/cursor/scope.d.ts.map +1 -1
- package/dist/charts/cursor/store.d.ts +5 -1
- package/dist/charts/cursor/store.d.ts.map +1 -1
- package/dist/charts/cursor/store.js +4 -4
- package/dist/charts/hooks/useChartCursor.d.ts.map +1 -1
- package/dist/charts/hooks/useChartCursor.js +9 -8
- package/dist/charts/index.d.ts +2 -2
- package/dist/charts/index.d.ts.map +1 -1
- package/dist/charts/primitives/ChartCard.d.ts +34 -9
- package/dist/charts/primitives/ChartCard.d.ts.map +1 -1
- package/dist/charts/primitives/ChartCard.js +29 -114
- package/dist/charts/primitives/ChartCard.test.d.ts +2 -0
- package/dist/charts/primitives/ChartCard.test.d.ts.map +1 -0
- package/dist/charts/sparklines/BarSparkline.d.ts +26 -2
- package/dist/charts/sparklines/BarSparkline.d.ts.map +1 -1
- package/dist/charts/sparklines/BarSparkline.js +18 -4
- package/dist/cli/check-coverage.test.d.ts +2 -0
- package/dist/cli/check-coverage.test.d.ts.map +1 -0
- package/dist/cli/index.d.ts +67 -4
- package/dist/cli/index.d.ts.map +1 -1
- package/dist/cli/index.js +225 -7
- package/dist/connectivity/connectivity-indicator.d.ts +4 -1
- package/dist/connectivity/connectivity-indicator.d.ts.map +1 -1
- package/dist/connectivity/connectivity-indicator.js +1 -1
- package/dist/content/index.d.ts +5 -3
- package/dist/content/index.d.ts.map +1 -1
- package/dist/content/index.js +4 -2
- package/dist/content/prose.module.css +7 -28
- package/dist/controls/actions.d.ts +169 -0
- package/dist/controls/actions.d.ts.map +1 -0
- package/dist/controls/actions.js +287 -0
- package/dist/controls/actions.module.css +62 -0
- package/dist/controls/actions.module.css.d.ts +13 -0
- package/dist/controls/actions.test.d.ts +2 -0
- package/dist/controls/actions.test.d.ts.map +1 -0
- package/dist/controls/actions.type-guard.test.d.ts +2 -0
- package/dist/controls/actions.type-guard.test.d.ts.map +1 -0
- package/dist/controls/compare-filter.d.ts +41 -0
- package/dist/controls/compare-filter.d.ts.map +1 -0
- package/dist/controls/compare-filter.js +23 -0
- package/dist/controls/control-group.d.ts +19 -0
- package/dist/controls/control-group.d.ts.map +1 -0
- package/dist/controls/control-group.js +20 -0
- package/dist/controls/control-group.test.d.ts +2 -0
- package/dist/controls/control-group.test.d.ts.map +1 -0
- package/dist/controls/controls.module.css +542 -0
- package/dist/controls/controls.module.css.d.ts +35 -0
- package/dist/controls/controls.router.test.d.ts +2 -0
- package/dist/controls/controls.router.test.d.ts.map +1 -0
- package/dist/controls/enum-filter.d.ts +30 -0
- package/dist/controls/enum-filter.d.ts.map +1 -0
- package/dist/controls/enum-filter.js +91 -0
- package/dist/controls/filter-context.d.ts +55 -0
- package/dist/controls/filter-context.d.ts.map +1 -0
- package/dist/controls/filter-context.js +69 -0
- package/dist/controls/filter-pill.d.ts +28 -0
- package/dist/controls/filter-pill.d.ts.map +1 -0
- package/dist/controls/filter-pill.js +70 -0
- package/dist/controls/filter-set.d.ts +13 -0
- package/dist/controls/filter-set.d.ts.map +1 -0
- package/dist/controls/filter-set.js +125 -0
- package/dist/controls/filter-set.test.d.ts +2 -0
- package/dist/controls/filter-set.test.d.ts.map +1 -0
- package/dist/controls/filter-sheet.d.ts +117 -0
- package/dist/controls/filter-sheet.d.ts.map +1 -0
- package/dist/controls/filter-sheet.js +146 -0
- package/dist/controls/glyphs.d.ts +27 -0
- package/dist/controls/glyphs.d.ts.map +1 -0
- package/dist/controls/glyphs.js +49 -0
- package/dist/controls/index.d.ts +19 -0
- package/dist/controls/index.d.ts.map +1 -0
- package/dist/controls/index.js +44 -0
- package/dist/controls/multi-select-filter.d.ts +23 -0
- package/dist/controls/multi-select-filter.d.ts.map +1 -0
- package/dist/controls/multi-select-filter.js +89 -0
- package/dist/controls/peer-boundary.test.d.ts +2 -0
- package/dist/controls/peer-boundary.test.d.ts.map +1 -0
- package/dist/controls/props-contract.test.d.ts +2 -0
- package/dist/controls/props-contract.test.d.ts.map +1 -0
- package/dist/controls/range-filter.d.ts +29 -0
- package/dist/controls/range-filter.d.ts.map +1 -0
- package/dist/controls/range-filter.js +120 -0
- package/dist/controls/search-filter.d.ts +10 -0
- package/dist/controls/search-filter.d.ts.map +1 -0
- package/dist/controls/search-filter.js +81 -0
- package/dist/controls/select-filter.d.ts +74 -0
- package/dist/controls/select-filter.d.ts.map +1 -0
- package/dist/controls/select-filter.js +10 -0
- package/dist/controls/sync-button.d.ts +38 -0
- package/dist/controls/sync-button.d.ts.map +1 -0
- package/dist/controls/sync-button.js +119 -0
- package/dist/controls/sync-button.module.css +63 -0
- package/dist/controls/sync-button.module.css.d.ts +11 -0
- package/dist/controls/sync-button.test.d.ts +2 -0
- package/dist/controls/sync-button.test.d.ts.map +1 -0
- package/dist/controls/toggle-filter.d.ts +9 -0
- package/dist/controls/toggle-filter.d.ts.map +1 -0
- package/dist/controls/toggle-filter.js +42 -0
- package/dist/controls/view-tabs.d.ts +23 -0
- package/dist/controls/view-tabs.d.ts.map +1 -0
- package/dist/controls/view-tabs.js +59 -0
- package/dist/controls-dates/date-range-picker.d.ts +4 -0
- package/dist/controls-dates/date-range-picker.d.ts.map +1 -0
- package/dist/controls-dates/date-range-picker.js +18 -0
- package/dist/controls-dates/index.d.ts +9 -0
- package/dist/controls-dates/index.d.ts.map +1 -0
- package/dist/controls-dates/index.js +4 -0
- package/dist/dashboard/delta-badge.d.ts +6 -12
- package/dist/dashboard/delta-badge.d.ts.map +1 -1
- package/dist/dashboard/delta-badge.js +1 -50
- package/dist/dashboard/query-state.d.ts +1 -1
- package/dist/dashboard/settings-section.d.ts +6 -4
- package/dist/dashboard/settings-section.d.ts.map +1 -1
- package/dist/dashboard/settings-section.js +23 -16
- package/dist/dashboard/settings-section.module.css +10 -0
- package/dist/dashboard/settings-section.module.css.d.ts +1 -0
- package/dist/dashboard/settings-section.test.d.ts +2 -0
- package/dist/dashboard/settings-section.test.d.ts.map +1 -0
- package/dist/dashboard/stat-card.d.ts +44 -9
- package/dist/dashboard/stat-card.d.ts.map +1 -1
- package/dist/dashboard/stat-card.js +80 -48
- package/dist/dashboard/stat-card.module.css +82 -0
- package/dist/dashboard/stat-card.module.css.d.ts +11 -0
- package/dist/data/data-table.d.ts +102 -20
- package/dist/data/data-table.d.ts.map +1 -1
- package/dist/data/data-table.js +166 -65
- package/dist/data/table.d.ts +1 -1
- package/dist/data/table.d.ts.map +1 -1
- package/dist/guard/grace.test.d.ts +20 -0
- package/dist/guard/grace.test.d.ts.map +1 -0
- package/dist/guard/index.d.ts +66 -3
- package/dist/guard/index.d.ts.map +1 -1
- package/dist/guard/index.js +82 -7
- package/dist/guard/types.d.ts +3 -3
- package/dist/guard/types.d.ts.map +1 -1
- package/dist/index.d.ts +12 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +13 -10
- package/dist/nav/types.d.ts +78 -2
- package/dist/nav/types.d.ts.map +1 -1
- package/dist/notifications/bell.d.ts +4 -1
- package/dist/notifications/bell.d.ts.map +1 -1
- package/dist/notifications/index.d.ts +1 -1
- package/dist/router-tanstack/field.d.ts +14 -0
- package/dist/router-tanstack/field.d.ts.map +1 -0
- package/dist/router-tanstack/field.js +4 -0
- package/dist/router-tanstack/index.d.ts +6 -2
- package/dist/router-tanstack/index.d.ts.map +1 -1
- package/dist/router-tanstack/index.js +4 -0
- package/dist/router-tanstack/multi-search-param-store.d.ts +4 -52
- package/dist/router-tanstack/multi-search-param-store.d.ts.map +1 -1
- package/dist/router-tanstack/multi-search-param-store.js +19 -27
- package/dist/router-tanstack/search-param-store.d.ts +3 -87
- package/dist/router-tanstack/search-param-store.d.ts.map +1 -1
- package/dist/router-tanstack/search-param-store.js +16 -45
- package/dist/router-tanstack/search-store.d.ts +81 -0
- package/dist/router-tanstack/search-store.d.ts.map +1 -0
- package/dist/router-tanstack/search-store.js +187 -0
- package/dist/router-tanstack/search-store.router.test.d.ts +2 -0
- package/dist/router-tanstack/search-store.router.test.d.ts.map +1 -0
- package/dist/router-tanstack/search-store.test.d.ts +2 -0
- package/dist/router-tanstack/search-store.test.d.ts.map +1 -0
- package/dist/router-tanstack/use-nav.d.ts +0 -2
- package/dist/router-tanstack/use-nav.d.ts.map +1 -1
- package/dist/router-tanstack/use-nav.js +34 -12
- package/dist/router-tanstack/use-nav.test.d.ts +2 -0
- package/dist/router-tanstack/use-nav.test.d.ts.map +1 -0
- package/dist/section/index.d.ts +2 -0
- package/dist/section/index.d.ts.map +1 -0
- package/dist/section/index.js +4 -0
- package/dist/section/section.d.ts +24 -0
- package/dist/section/section.d.ts.map +1 -0
- package/dist/section/section.js +126 -0
- package/dist/section/section.module.css +104 -0
- package/dist/section/section.module.css.d.ts +11 -0
- package/dist/section/section.test.d.ts +2 -0
- package/dist/section/section.test.d.ts.map +1 -0
- package/dist/shell/app-breadcrumbs.d.ts.map +1 -1
- package/dist/shell/app-breadcrumbs.js +18 -15
- package/dist/shell/app-header.module.css +77 -37
- package/dist/shell/app-header.module.css.d.ts +2 -1
- package/dist/shell/app-mobile-nav.d.ts +9 -2
- package/dist/shell/app-mobile-nav.d.ts.map +1 -1
- package/dist/shell/app-mobile-nav.js +98 -11
- package/dist/shell/app-mobile-nav.module.css +8 -0
- package/dist/shell/app-mobile-nav.module.css.d.ts +1 -0
- package/dist/shell/app-sidebar.d.ts +55 -28
- package/dist/shell/app-sidebar.d.ts.map +1 -1
- package/dist/shell/app-sidebar.js +116 -67
- package/dist/shell/app-sidebar.module.css +303 -20
- package/dist/shell/app-sidebar.module.css.d.ts +19 -1
- package/dist/shell/index.d.ts +55 -21
- package/dist/shell/index.d.ts.map +1 -1
- package/dist/shell/index.js +40 -18
- package/dist/shell/mobile-nav-model.d.ts +13 -2
- package/dist/shell/mobile-nav-model.d.ts.map +1 -1
- package/dist/shell/mobile-nav-model.js +15 -1
- package/dist/shell/nav-count-badge.d.ts +1 -1
- package/dist/shell/page-bar.d.ts +51 -0
- package/dist/shell/page-bar.d.ts.map +1 -0
- package/dist/shell/page-bar.js +154 -0
- package/dist/shell/page-bar.module.css +178 -0
- package/dist/shell/page-bar.module.css.d.ts +17 -0
- package/dist/shell/page-bar.test.d.ts +2 -0
- package/dist/shell/page-bar.test.d.ts.map +1 -0
- package/dist/shell/sidebar-block-model.d.ts +48 -0
- package/dist/shell/sidebar-block-model.d.ts.map +1 -0
- package/dist/shell/sidebar-block-model.js +39 -0
- package/dist/shell/sidebar-blocks.d.ts +32 -0
- package/dist/shell/sidebar-blocks.d.ts.map +1 -0
- package/dist/shell/sidebar-blocks.js +169 -0
- package/dist/shell/sidebar-search.d.ts +13 -1
- package/dist/shell/sidebar-search.d.ts.map +1 -1
- package/dist/shell/sidebar-search.js +34 -1
- package/dist/shell/sidebar-search.module.css +26 -0
- package/dist/shell/sidebar-search.module.css.d.ts +2 -0
- package/dist/state/fields.d.ts +392 -0
- package/dist/state/fields.d.ts.map +1 -0
- package/dist/state/fields.js +369 -0
- package/dist/state/fields.test.d.ts +2 -0
- package/dist/state/fields.test.d.ts.map +1 -0
- package/dist/state/persisted.d.ts +48 -0
- package/dist/state/persisted.d.ts.map +1 -0
- package/dist/state/persisted.js +105 -0
- package/dist/state.d.ts +11 -56
- package/dist/state.d.ts.map +1 -1
- package/dist/state.js +14 -120
- package/dist/styles.css +13 -0
- package/dist/surfaces.d.ts +106 -24
- package/dist/surfaces.d.ts.map +1 -1
- package/dist/surfaces.js +155 -19
- package/dist/theme/controls.module.css +16 -1
- package/dist/theme/ctl-theme.d.ts +32 -0
- package/dist/theme/ctl-theme.d.ts.map +1 -0
- package/dist/theme/ctl-theme.js +54 -0
- package/dist/theme/ctl-theme.test.d.ts +2 -0
- package/dist/theme/ctl-theme.test.d.ts.map +1 -0
- package/dist/theme/ctl-tier-coverage.test.d.ts +2 -0
- package/dist/theme/ctl-tier-coverage.test.d.ts.map +1 -0
- package/dist/theme/divider-law.test.d.ts +2 -0
- package/dist/theme/divider-law.test.d.ts.map +1 -0
- package/dist/theme/icon-slot.d.ts +44 -0
- package/dist/theme/icon-slot.d.ts.map +1 -0
- package/dist/theme/icon-slot.js +21 -0
- package/dist/theme/icon-slot.module.css +47 -0
- package/dist/theme/icon-slot.module.css.d.ts +8 -0
- package/dist/theme/icon-slot.test.d.ts +2 -0
- package/dist/theme/icon-slot.test.d.ts.map +1 -0
- package/dist/theme/index.d.ts +1 -0
- package/dist/theme/index.d.ts.map +1 -1
- package/dist/theme/index.js +82 -5
- package/dist/theme/layout-rhythm-css.test.d.ts +2 -0
- package/dist/theme/layout-rhythm-css.test.d.ts.map +1 -0
- package/dist/theme/segmented-control.module.css +20 -0
- package/dist/theme/shadow-surfaces.test.d.ts.map +1 -1
- package/dist/tokens/index.d.ts +12 -0
- package/dist/tokens/index.d.ts.map +1 -1
- package/dist/tokens/index.js +38 -8
- package/dist/tokens/palette.d.ts +97 -61
- package/dist/tokens/palette.d.ts.map +1 -1
- package/dist/tokens/palette.js +76 -38
- package/dist/tokens.css +10 -2
- package/dist/widget-header/delta-badge.d.ts +13 -0
- package/dist/widget-header/delta-badge.d.ts.map +1 -0
- package/dist/widget-header/delta-badge.js +29 -0
- package/dist/widget-header/delta-badge.test.d.ts +2 -0
- package/dist/widget-header/delta-badge.test.d.ts.map +1 -0
- package/dist/widget-header/index.d.ts +3 -0
- package/dist/widget-header/index.d.ts.map +1 -0
- package/dist/widget-header/index.js +6 -0
- package/dist/widget-header/no-mantine.test.d.ts +2 -0
- package/dist/widget-header/no-mantine.test.d.ts.map +1 -0
- package/dist/widget-header/widget-header.d.ts +37 -0
- package/dist/widget-header/widget-header.d.ts.map +1 -0
- package/dist/widget-header/widget-header.js +77 -0
- package/dist/widget-header/widget-header.module.css +335 -0
- package/dist/widget-header/widget-header.module.css.d.ts +24 -0
- package/dist/widget-header/widget-header.test.d.ts +2 -0
- package/dist/widget-header/widget-header.test.d.ts.map +1 -0
- package/llms.txt +30 -17
- package/package.json +14 -1
- package/src/charts/cursor/resolve.test.ts +29 -1
- package/src/charts/cursor/resolve.ts +38 -6
- package/src/charts/cursor/scope.tsx +6 -3
- package/src/charts/cursor/store.test.ts +19 -1
- package/src/charts/cursor/store.ts +10 -5
- package/src/charts/hooks/useChartCursor.ts +16 -8
- package/src/charts/index.ts +2 -2
- package/src/charts/primitives/CartesianChart.test.tsx +124 -0
- package/src/charts/primitives/ChartCard.test.tsx +66 -0
- package/src/charts/primitives/ChartCard.tsx +86 -146
- package/src/charts/sparklines/BarSparkline.tsx +43 -5
- package/src/cli/check-coverage.test.ts +297 -0
- package/src/cli/check-theme.test.ts +38 -14
- package/src/cli/index.ts +392 -16
- package/src/cli/init-sync.test.ts +97 -0
- package/src/connectivity/connectivity-indicator.tsx +16 -9
- package/src/content/index.ts +16 -3
- package/src/content/prose.module.css +7 -28
- package/src/content/prose.module.css.test.ts +8 -24
- package/src/controls/actions.module.css +62 -0
- package/src/controls/actions.module.css.d.ts +13 -0
- package/src/controls/actions.test.tsx +252 -0
- package/src/controls/actions.tsx +655 -0
- package/src/controls/actions.type-guard.test.ts +63 -0
- package/src/controls/compare-filter.tsx +64 -0
- package/src/controls/control-group.test.tsx +229 -0
- package/src/controls/control-group.tsx +63 -0
- package/src/controls/controls.module.css +542 -0
- package/src/controls/controls.module.css.d.ts +35 -0
- package/src/controls/controls.router.test.tsx +637 -0
- package/src/controls/enum-filter.tsx +159 -0
- package/src/controls/filter-context.tsx +153 -0
- package/src/controls/filter-pill.tsx +120 -0
- package/src/controls/filter-set.test.tsx +472 -0
- package/src/controls/filter-set.tsx +225 -0
- package/src/controls/filter-sheet.tsx +285 -0
- package/src/controls/glyphs.tsx +81 -0
- package/src/controls/index.ts +48 -0
- package/src/controls/multi-select-filter.tsx +140 -0
- package/src/controls/peer-boundary.test.ts +85 -0
- package/src/controls/props-contract.test.tsx +106 -0
- package/src/controls/range-filter.tsx +184 -0
- package/src/controls/search-filter.tsx +123 -0
- package/src/controls/select-filter.tsx +89 -0
- package/src/controls/sync-button.module.css +63 -0
- package/src/controls/sync-button.module.css.d.ts +11 -0
- package/src/controls/sync-button.test.tsx +183 -0
- package/src/controls/sync-button.tsx +197 -0
- package/src/controls/toggle-filter.tsx +60 -0
- package/src/controls/view-tabs.tsx +110 -0
- package/src/controls-dates/date-range-picker.tsx +42 -0
- package/src/controls-dates/index.ts +8 -0
- package/src/dashboard/delta-badge.tsx +4 -74
- package/src/dashboard/query-state.tsx +1 -1
- package/src/dashboard/settings-section.module.css +10 -0
- package/src/dashboard/settings-section.module.css.d.ts +1 -0
- package/src/dashboard/settings-section.test.tsx +56 -0
- package/src/dashboard/settings-section.tsx +33 -30
- package/src/dashboard/stat-card.module.css +82 -0
- package/src/dashboard/stat-card.module.css.d.ts +11 -0
- package/src/dashboard/stat-card.test.tsx +67 -1
- package/src/dashboard/stat-card.tsx +209 -68
- package/src/data/data-table.test.tsx +282 -6
- package/src/data/data-table.tsx +429 -94
- package/src/data/table.ts +2 -0
- package/src/guard/check-source.test.ts +126 -13
- package/src/guard/grace.test.ts +149 -0
- package/src/guard/index.ts +172 -29
- package/src/guard/types.ts +4 -2
- package/src/index.ts +46 -5
- package/src/nav/types.ts +84 -2
- package/src/notifications/bell.tsx +6 -3
- package/src/notifications/index.ts +1 -1
- package/src/router-tanstack/field.ts +31 -0
- package/src/router-tanstack/index.ts +24 -2
- package/src/router-tanstack/multi-search-param-store.test.ts +71 -3
- package/src/router-tanstack/multi-search-param-store.ts +46 -77
- package/src/router-tanstack/search-param-store.ts +42 -159
- package/src/router-tanstack/search-store.router.test.tsx +661 -0
- package/src/router-tanstack/search-store.test.ts +418 -0
- package/src/router-tanstack/search-store.ts +380 -0
- package/src/router-tanstack/use-nav.test.tsx +187 -0
- package/src/router-tanstack/use-nav.ts +72 -14
- package/src/section/index.ts +1 -0
- package/src/section/section.module.css +104 -0
- package/src/section/section.module.css.d.ts +11 -0
- package/src/section/section.test.tsx +204 -0
- package/src/section/section.tsx +220 -0
- package/src/shell/app-breadcrumbs.tsx +44 -31
- package/src/shell/app-header.module.css +77 -37
- package/src/shell/app-header.module.css.d.ts +2 -1
- package/src/shell/app-mobile-nav.module.css +8 -0
- package/src/shell/app-mobile-nav.module.css.d.ts +1 -0
- package/src/shell/app-mobile-nav.tsx +147 -8
- package/src/shell/app-sidebar-account.tsx +2 -2
- package/src/shell/app-sidebar.module.css +303 -20
- package/src/shell/app-sidebar.module.css.d.ts +19 -1
- package/src/shell/app-sidebar.test.tsx +456 -41
- package/src/shell/app-sidebar.tsx +276 -140
- package/src/shell/index.test.tsx +291 -5
- package/src/shell/index.tsx +137 -38
- package/src/shell/mobile-nav-model.test.ts +228 -7
- package/src/shell/mobile-nav-model.ts +53 -3
- package/src/shell/nav-count-badge.tsx +1 -1
- package/src/shell/page-bar.module.css +178 -0
- package/src/shell/page-bar.module.css.d.ts +17 -0
- package/src/shell/page-bar.test.tsx +316 -0
- package/src/shell/page-bar.tsx +315 -0
- package/src/shell/sidebar-block-model.ts +83 -0
- package/src/shell/sidebar-blocks.tsx +301 -0
- package/src/shell/sidebar-search.module.css +26 -0
- package/src/shell/sidebar-search.module.css.d.ts +2 -0
- package/src/shell/sidebar-search.tsx +71 -2
- package/src/state/fields.test.tsx +258 -0
- package/src/state/fields.ts +892 -0
- package/src/state/persisted.ts +211 -0
- package/src/state.ts +46 -240
- package/src/styles.css +13 -0
- package/src/surfaces.ts +203 -36
- package/src/theme/controls.module.css +16 -1
- package/src/theme/ctl-theme.test.tsx +275 -0
- package/src/theme/ctl-theme.tsx +138 -0
- package/src/theme/ctl-tier-coverage.test.ts +170 -0
- package/src/theme/density-relations.test.ts +106 -51
- package/src/theme/divider-law.test.ts +259 -0
- package/src/theme/icon-slot.module.css +47 -0
- package/src/theme/icon-slot.module.css.d.ts +8 -0
- package/src/theme/icon-slot.test.tsx +275 -0
- package/src/theme/icon-slot.tsx +59 -0
- package/src/theme/index.ts +165 -3
- package/src/theme/layout-rhythm-css.test.ts +72 -0
- package/src/theme/radius.test.ts +11 -1
- package/src/theme/segmented-control.module.css +20 -0
- package/src/theme/shadow-surfaces.test.ts +20 -7
- package/src/theme/spacing.test.ts +75 -27
- package/src/theme/use-basalt-spacing.ts +2 -2
- package/src/tokens/density.test.ts +7 -25
- package/src/tokens/index.ts +48 -8
- package/src/tokens/palette.ts +107 -73
- package/src/widget-header/delta-badge.test.tsx +60 -0
- package/src/widget-header/delta-badge.tsx +57 -0
- package/src/widget-header/index.ts +2 -0
- package/src/widget-header/no-mantine.test.ts +33 -0
- package/src/widget-header/widget-header.module.css +335 -0
- package/src/widget-header/widget-header.module.css.d.ts +24 -0
- package/src/widget-header/widget-header.test.tsx +187 -0
- package/src/widget-header/widget-header.tsx +168 -0
- package/agent/rules/basalt-agent.md +0 -452
- package/agent/rules/basalt-app.md +0 -246
- package/agent/rules/basalt-commands.md +0 -320
- package/agent/rules/basalt-content.md +0 -409
- package/agent/rules/basalt-data.md +0 -269
- package/agent/rules/basalt-forms.md +0 -186
- package/agent/rules/basalt-notifications.md +0 -264
- package/agent/rules/basalt-query.md +0 -187
- package/agent/rules/basalt-router.md +0 -336
- package/dist/content/article-filter-bar.d.ts +0 -40
- package/dist/content/article-filter-bar.d.ts.map +0 -1
- package/dist/content/article-filter-bar.js +0 -56
- package/dist/shell/page-header.d.ts +0 -13
- package/dist/shell/page-header.d.ts.map +0 -1
- package/dist/shell/page-header.js +0 -23
- package/src/content/article-filter-bar.tsx +0 -88
- package/src/shell/page-header.tsx +0 -35
package/src/cli/index.ts
CHANGED
|
@@ -5,10 +5,12 @@
|
|
|
5
5
|
* BasaltConfig, builds a GuardConfig, walks the source roots, calls `checkSource` per file,
|
|
6
6
|
* collects Finding[], groups/reports findings, and returns an exit code.
|
|
7
7
|
*
|
|
8
|
-
* `checkCoverage` asserts
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
8
|
+
* `checkCoverage` asserts 11 invariants: guardKinds ⊆ GUARD_RULES, rule files on disk, skill files
|
|
9
|
+
* on disk, subpath-export-coverage, exports→SURFACES reverse, globs required for non-empty
|
|
10
|
+
* forbiddenImports, headless Mantine-ban completeness, optionalPeers
|
|
11
|
+
* peerDependencies/peerDependenciesMeta presence, every plugin rule on exactly one surface, every
|
|
12
|
+
* guard kind on at least one, and the agent-layer line budgets. See `checkCoverage`'s own JSDoc for
|
|
13
|
+
* what each one buys.
|
|
12
14
|
*
|
|
13
15
|
* `init` / `sync` scaffold and reconcile the framework's *agentic* surface into a consumer repo:
|
|
14
16
|
* Claude Code rules + skills, a managed CLAUDE.md block, a DESIGN.md seed, and the toolchain
|
|
@@ -26,6 +28,7 @@ import {
|
|
|
26
28
|
mkdirSync,
|
|
27
29
|
readdirSync,
|
|
28
30
|
readFileSync,
|
|
31
|
+
rmdirSync,
|
|
29
32
|
statSync,
|
|
30
33
|
unlinkSync,
|
|
31
34
|
writeFileSync,
|
|
@@ -47,8 +50,14 @@ import {
|
|
|
47
50
|
} from '../guard'
|
|
48
51
|
import type { Finding, GuardConfig, GuardKind, GuardSeverity } from '../guard'
|
|
49
52
|
import { evaluateGuardHook } from '../guard/guard-hook'
|
|
50
|
-
import {
|
|
51
|
-
|
|
53
|
+
import {
|
|
54
|
+
PLUGIN_RULE_ID_LIST,
|
|
55
|
+
RULE_NAMES,
|
|
56
|
+
SKILL_NAMES,
|
|
57
|
+
SURFACES,
|
|
58
|
+
TOKEN_LAYER_BOUNDARY_SURFACES,
|
|
59
|
+
} from '../surfaces'
|
|
60
|
+
import type { DoctrineSpec, RuleName, SurfaceSpec } from '../surfaces'
|
|
52
61
|
import { buildPaletteCss, deriveSpacing } from '../tokens'
|
|
53
62
|
|
|
54
63
|
/** Shape of the optional `"basalt"` key in a consumer's package.json. */
|
|
@@ -1838,6 +1847,113 @@ function migrateLegacyOxfmt(cwd: string, pkgRoot: string, manifest: Manifest): v
|
|
|
1838
1847
|
if (shipped !== null && onDisk === shipped) unlinkSync(legacyAbs)
|
|
1839
1848
|
}
|
|
1840
1849
|
|
|
1850
|
+
/**
|
|
1851
|
+
* The two managed NAMESPACES whose membership is derived, not fixed: `RULE_NAMES` and `SKILL_NAMES`
|
|
1852
|
+
* decide which files `managedFiles()` places under them, so a rule or skill that is RETIRED
|
|
1853
|
+
* upstream leaves a file behind in every consumer that ever synced it — and Claude keeps loading it.
|
|
1854
|
+
*
|
|
1855
|
+
* Scoped to these two patterns on purpose, and never applied to the manifest wholesale: several
|
|
1856
|
+
* managed/seed entries are legitimately absent from `managedFiles()` for a run (`lefthook.yml` and
|
|
1857
|
+
* `check.yml` when the package is not the repo root, `src/query-client.ts` when it was relocated,
|
|
1858
|
+
* the scaffolds without their peers). Deleting a dest just because this run did not place it would
|
|
1859
|
+
* remove a consumer's CI workflow on a monorepo.
|
|
1860
|
+
*/
|
|
1861
|
+
const RETIREABLE_DEST_PATTERNS: readonly RegExp[] = [
|
|
1862
|
+
/^\.claude\/rules\/basalt-[a-z0-9-]+\.md$/,
|
|
1863
|
+
/^\.claude\/skills\/basalt-[a-z0-9-]+\/SKILL\.md$/,
|
|
1864
|
+
]
|
|
1865
|
+
|
|
1866
|
+
/** A tracked dest inside a derived namespace that this basalt version no longer ships. */
|
|
1867
|
+
export function retiredManagedDests(
|
|
1868
|
+
manifest: Manifest,
|
|
1869
|
+
files: readonly ManagedFile[],
|
|
1870
|
+
): readonly string[] {
|
|
1871
|
+
const shipped = new Set(files.map((f) => f.dest))
|
|
1872
|
+
return Object.keys(manifest.files)
|
|
1873
|
+
.filter((dest) => !shipped.has(dest))
|
|
1874
|
+
.filter((dest) => RETIREABLE_DEST_PATTERNS.some((re) => re.test(dest)))
|
|
1875
|
+
.toSorted()
|
|
1876
|
+
}
|
|
1877
|
+
|
|
1878
|
+
/** Outcome of one prune pass — `removed` was deleted, `drifted` was locally edited and left alone. */
|
|
1879
|
+
type PruneResult = { removed: string[]; drifted: string[] }
|
|
1880
|
+
|
|
1881
|
+
/**
|
|
1882
|
+
* Report a prune pass. Always names the files: a doctrine file DISAPPEARING from a consumer's
|
|
1883
|
+
* `.claude/` is exactly the class of change the sync diff exists to be reviewed, and `9 removed`
|
|
1884
|
+
* with no names is what made the kept-files line a false green before 1.23.0.
|
|
1885
|
+
*/
|
|
1886
|
+
function reportPrune(pruned: PruneResult): void {
|
|
1887
|
+
if (pruned.removed.length > 0) {
|
|
1888
|
+
console.log(
|
|
1889
|
+
`\nRetired by this basalt-ui version (deleted — they no longer ship):\n` +
|
|
1890
|
+
pruned.removed.map((dest) => ` · ${dest}`).join('\n'),
|
|
1891
|
+
)
|
|
1892
|
+
}
|
|
1893
|
+
if (pruned.drifted.length === 0) return
|
|
1894
|
+
console.log(
|
|
1895
|
+
`\nRetired but locally edited, so left in place — your agent still loads them. Delete them, ` +
|
|
1896
|
+
'or run `basalt-ui sync --force`:\n' +
|
|
1897
|
+
pruned.drifted.map((dest) => ` · ${dest}`).join('\n'),
|
|
1898
|
+
)
|
|
1899
|
+
}
|
|
1900
|
+
|
|
1901
|
+
/**
|
|
1902
|
+
* Delete the managed rule/skill files this basalt version retired, and drop their manifest entries.
|
|
1903
|
+
*
|
|
1904
|
+
* The 13→6 rule merge (`docs/CONTROLS-SPEC.md` §7) is the case this exists for: nine
|
|
1905
|
+
* `.claude/rules/basalt-*.md` files stopped shipping in one minor, and without this a consumer's
|
|
1906
|
+
* agent would go on reading `basalt-router.md`'s superseded placement doctrine forever, with
|
|
1907
|
+
* `sync --check` reporting green because nothing in the ledger asks about a dest the run no longer
|
|
1908
|
+
* places.
|
|
1909
|
+
*
|
|
1910
|
+
* Same three-way discipline as every other managed unit, so a consumer edit is never discarded
|
|
1911
|
+
* silently: an untouched file (matching its recorded hash, raw or normalized) is deleted and its
|
|
1912
|
+
* entry dropped; a locally-edited one is LEFT in place, reported, and keeps its entry so
|
|
1913
|
+
* `sync --check` stays red and `--force` can still finish the job. `dryRun` is `--check`.
|
|
1914
|
+
*/
|
|
1915
|
+
function pruneRetiredManagedFiles(
|
|
1916
|
+
cwd: string,
|
|
1917
|
+
manifest: Manifest,
|
|
1918
|
+
files: readonly ManagedFile[],
|
|
1919
|
+
opts: { dryRun?: boolean; force?: boolean } = {},
|
|
1920
|
+
): PruneResult {
|
|
1921
|
+
const result: PruneResult = { removed: [], drifted: [] }
|
|
1922
|
+
for (const dest of retiredManagedDests(manifest, files)) {
|
|
1923
|
+
const abs = resolve(cwd, dest)
|
|
1924
|
+
const current = readIfExists(abs)
|
|
1925
|
+
const recorded = manifest.files[dest]
|
|
1926
|
+
const untouched =
|
|
1927
|
+
current === null ||
|
|
1928
|
+
sha256(current) === recorded ||
|
|
1929
|
+
sha256(normalizeForLedger(current)) === recorded
|
|
1930
|
+
if (!untouched && opts.force !== true) {
|
|
1931
|
+
result.drifted.push(dest)
|
|
1932
|
+
continue
|
|
1933
|
+
}
|
|
1934
|
+
result.removed.push(dest)
|
|
1935
|
+
if (opts.dryRun === true) continue
|
|
1936
|
+
delete manifest.files[dest]
|
|
1937
|
+
if (current !== null) unlinkSync(abs)
|
|
1938
|
+
removeEmptyParentDir(abs)
|
|
1939
|
+
}
|
|
1940
|
+
return result
|
|
1941
|
+
}
|
|
1942
|
+
|
|
1943
|
+
/**
|
|
1944
|
+
* Remove a now-empty `.claude/skills/basalt-x/` directory after its `SKILL.md` was pruned. Silent by
|
|
1945
|
+
* design — a leftover empty directory is cosmetic, and a consumer file sitting beside the skill is a
|
|
1946
|
+
* reason to keep it, not an error to report.
|
|
1947
|
+
*/
|
|
1948
|
+
function removeEmptyParentDir(fileAbs: string): void {
|
|
1949
|
+
const dir = dirname(fileAbs)
|
|
1950
|
+
try {
|
|
1951
|
+
if (readdirSync(dir).length === 0) rmdirSync(dir)
|
|
1952
|
+
} catch {
|
|
1953
|
+
/* not empty, not readable, or already gone — nothing to clean up */
|
|
1954
|
+
}
|
|
1955
|
+
}
|
|
1956
|
+
|
|
1841
1957
|
/** The frozen `> Managed by basalt-ui (1.9.0).` opener every pre-fix DESIGN.md seed carries. */
|
|
1842
1958
|
const DESIGN_VERSION_LINE = /^(> Managed by basalt-ui) \(\d+\.\d+\.\d+\)\./m
|
|
1843
1959
|
|
|
@@ -2506,6 +2622,11 @@ export function init(cwd: string = process.cwd(), scaffoldFlags: ScaffoldFlags =
|
|
|
2506
2622
|
writtenFiles.push(file.dest)
|
|
2507
2623
|
}
|
|
2508
2624
|
|
|
2625
|
+
// A re-`init` over an existing install reconciles the derived namespaces too — otherwise the only
|
|
2626
|
+
// route out of a retired rule file is `sync`, and `init` is what a consumer reaches for after an
|
|
2627
|
+
// upgrade that changed the doctrine set.
|
|
2628
|
+
const pruned = pruneRetiredManagedFiles(cwd, manifest, files)
|
|
2629
|
+
|
|
2509
2630
|
const mergeLint: MergeLintResult | null =
|
|
2510
2631
|
scaffoldFlags.mergeLint === true ? mergeOxlintExtends(cwd, ctx.vars.OXLINT_PRESET_PATH) : null
|
|
2511
2632
|
|
|
@@ -2516,6 +2637,7 @@ export function init(cwd: string = process.cwd(), scaffoldFlags: ScaffoldFlags =
|
|
|
2516
2637
|
console.log(
|
|
2517
2638
|
`basalt-ui init: ${writtenFiles.length} written, ${keptFiles.length} kept, manifest at ${MANIFEST_PATH}`,
|
|
2518
2639
|
)
|
|
2640
|
+
reportPrune(pruned)
|
|
2519
2641
|
if (missingSources.length > 0) {
|
|
2520
2642
|
console.log(
|
|
2521
2643
|
`basalt-ui init: ${missingSources.length} shipped asset(s) not present, skipped: ${missingSources.join(', ')}`,
|
|
@@ -2825,6 +2947,16 @@ export function sync(opts: SyncOptions = {}, invocationCwd: string = process.cwd
|
|
|
2825
2947
|
else created++
|
|
2826
2948
|
}
|
|
2827
2949
|
|
|
2950
|
+
// Retired rules/skills: the derived namespaces (RULE_NAMES / SKILL_NAMES) are the only managed
|
|
2951
|
+
// sets whose MEMBERSHIP moves between versions, so this is where an upstream deletion reaches a
|
|
2952
|
+
// consumer. Counted into `staleForCheck` so `sync --check` is red until it is applied — a rule
|
|
2953
|
+
// file basalt stopped shipping is drift, and the agent goes on reading it otherwise.
|
|
2954
|
+
const pruned = pruneRetiredManagedFiles(cwd, manifest, files, {
|
|
2955
|
+
dryRun: opts.check === true,
|
|
2956
|
+
force: opts.force === true,
|
|
2957
|
+
})
|
|
2958
|
+
staleForCheck += pruned.removed.length + pruned.drifted.length
|
|
2959
|
+
|
|
2828
2960
|
// Placement notices — informational, never affect the exit code (a skipped tooling seed or a
|
|
2829
2961
|
// relocated scaffold is a legitimate consumer choice, not a sync failure).
|
|
2830
2962
|
if (!placement.isPackageRepoRoot) {
|
|
@@ -2881,6 +3013,13 @@ export function sync(opts: SyncOptions = {}, invocationCwd: string = process.cwd
|
|
|
2881
3013
|
console.error('basalt-ui sync --check: locally-drifted managed files:')
|
|
2882
3014
|
for (const l of driftLines) console.error(l)
|
|
2883
3015
|
}
|
|
3016
|
+
const retired = [...pruned.removed, ...pruned.drifted]
|
|
3017
|
+
if (retired.length > 0) {
|
|
3018
|
+
console.error(
|
|
3019
|
+
`basalt-ui sync --check: ${retired.length} retired rule/skill file(s) still present: ` +
|
|
3020
|
+
`${retired.join(', ')} — run \`basalt-ui sync\`.`,
|
|
3021
|
+
)
|
|
3022
|
+
}
|
|
2884
3023
|
if (staleForCheck > 0) {
|
|
2885
3024
|
console.error(`basalt-ui sync --check: ${staleForCheck} managed file(s) out of date.`)
|
|
2886
3025
|
return 1
|
|
@@ -2895,8 +3034,9 @@ export function sync(opts: SyncOptions = {}, invocationCwd: string = process.cwd
|
|
|
2895
3034
|
|
|
2896
3035
|
console.log(
|
|
2897
3036
|
`basalt-ui sync: ${updated} updated, ${created} created, ${recreated} recreated, ` +
|
|
2898
|
-
`${skippedDrift} skipped (drift).`,
|
|
3037
|
+
`${skippedDrift} skipped (drift), ${pruned.removed.length} retired.`,
|
|
2899
3038
|
)
|
|
3039
|
+
reportPrune(pruned)
|
|
2900
3040
|
if (driftLines.length > 0) {
|
|
2901
3041
|
console.log('Locally-edited files were skipped (run with --force to overwrite):')
|
|
2902
3042
|
for (const l of driftLines) console.log(l)
|
|
@@ -2910,14 +3050,160 @@ export function sync(opts: SyncOptions = {}, invocationCwd: string = process.cwd
|
|
|
2910
3050
|
}
|
|
2911
3051
|
|
|
2912
3052
|
// ──────────────────────────────────────────────────────────────────────────────
|
|
2913
|
-
//
|
|
3053
|
+
// coverage headers — the generated `<!-- basalt:coverage -->` block per rule file
|
|
3054
|
+
// ──────────────────────────────────────────────────────────────────────────────
|
|
3055
|
+
|
|
3056
|
+
/** The block's delimiters. Both are HTML comments, so the block renders as nothing in a viewer. */
|
|
3057
|
+
export const COVERAGE_BLOCK_OPEN = '<!-- basalt:coverage -->'
|
|
3058
|
+
export const COVERAGE_BLOCK_CLOSE = '<!-- /basalt:coverage -->'
|
|
3059
|
+
|
|
3060
|
+
const COVERAGE_BLOCK_RE = /<!-- basalt:coverage -->[\s\S]*?<!-- \/basalt:coverage -->/
|
|
3061
|
+
|
|
3062
|
+
/**
|
|
3063
|
+
* What actually enforces one rule file's doctrine, as the file itself should state it.
|
|
3064
|
+
*
|
|
3065
|
+
* Derived from SURFACES — the union over every doctrine surface carrying that `rule`, because
|
|
3066
|
+
* `rule` is many-to-one (`data` covers `./data`, `./data/table`, `./data/virtual`; `mantine` covers
|
|
3067
|
+
* `.` and `./connectivity`). Naming only one surface's coverage would understate the rule.
|
|
3068
|
+
*/
|
|
3069
|
+
export function coverageFor(rule: RuleName): {
|
|
3070
|
+
guardKinds: string[]
|
|
3071
|
+
pluginRules: string[]
|
|
3072
|
+
advisoryLaws: string[]
|
|
3073
|
+
} {
|
|
3074
|
+
const specs = (Object.values(SURFACES) as SurfaceSpec[]).filter(
|
|
3075
|
+
(s): s is DoctrineSpec => s.kind === 'doctrine' && s.rule === rule,
|
|
3076
|
+
)
|
|
3077
|
+
const unique = (values: readonly string[]): string[] => [...new Set(values)].toSorted()
|
|
3078
|
+
return {
|
|
3079
|
+
guardKinds: unique(specs.flatMap((s) => [...s.guardKinds])),
|
|
3080
|
+
pluginRules: unique(specs.flatMap((s) => [...s.pluginRules])),
|
|
3081
|
+
// NOT sorted, and not deduped across surfaces by content: an advisory law is a sentence, and
|
|
3082
|
+
// its order is the order the spec argues it in.
|
|
3083
|
+
advisoryLaws: specs.flatMap((s) => [...(s.advisoryLaws ?? [])]),
|
|
3084
|
+
}
|
|
3085
|
+
}
|
|
3086
|
+
|
|
3087
|
+
/**
|
|
3088
|
+
* The generated block for one rule, verbatim — HTML comments only, one claim per line so a diff
|
|
3089
|
+
* points at the claim that changed rather than at a reflowed paragraph.
|
|
3090
|
+
*
|
|
3091
|
+
* `not guarded` is printed even when empty, and that is the point: a rule file whose block says
|
|
3092
|
+
* `not guarded: —` is claiming full coverage, which is a claim someone can check. A block that
|
|
3093
|
+
* simply omitted the section would read as "nothing to declare" whether or not anyone had looked
|
|
3094
|
+
* (D8 — see docs/CONTROLS-SPEC.md §6 "Honest coverage").
|
|
3095
|
+
*/
|
|
3096
|
+
export function coverageBlock(rule: RuleName): string {
|
|
3097
|
+
const { guardKinds, pluginRules, advisoryLaws } = coverageFor(rule)
|
|
3098
|
+
const backedBy = [
|
|
3099
|
+
guardKinds.length > 0 ? `guard kinds — ${guardKinds.join(', ')}` : 'guard kinds — none',
|
|
3100
|
+
pluginRules.length > 0
|
|
3101
|
+
? `oxlint rules — ${pluginRules.map((id) => `basalt/${id}`).join(', ')}`
|
|
3102
|
+
: 'oxlint rules — none',
|
|
3103
|
+
].join(' · ')
|
|
3104
|
+
const lines = [
|
|
3105
|
+
COVERAGE_BLOCK_OPEN,
|
|
3106
|
+
'<!-- GENERATED from src/surfaces.ts — `basalt-ui check-coverage --write`. Do not hand-edit. -->',
|
|
3107
|
+
`<!-- backed by: ${backedBy} -->`,
|
|
3108
|
+
]
|
|
3109
|
+
if (advisoryLaws.length === 0) lines.push('<!-- not guarded: — -->')
|
|
3110
|
+
else for (const law of advisoryLaws) lines.push(`<!-- not guarded: ${law} -->`)
|
|
3111
|
+
lines.push(COVERAGE_BLOCK_CLOSE)
|
|
3112
|
+
return lines.join('\n')
|
|
3113
|
+
}
|
|
3114
|
+
|
|
3115
|
+
/** The block a rule file currently carries, or null when it has none yet. */
|
|
3116
|
+
export function readCoverageBlock(text: string): string | null {
|
|
3117
|
+
return COVERAGE_BLOCK_RE.exec(text)?.[0] ?? null
|
|
3118
|
+
}
|
|
3119
|
+
|
|
3120
|
+
/**
|
|
3121
|
+
* `text` with `block` in it — replacing the existing block, or inserted directly below the YAML
|
|
3122
|
+
* frontmatter (the rule files' `paths:` header is load-bearing and must stay first).
|
|
3123
|
+
*/
|
|
3124
|
+
export function applyCoverageBlock(text: string, block: string): string {
|
|
3125
|
+
if (COVERAGE_BLOCK_RE.test(text)) return text.replace(COVERAGE_BLOCK_RE, block)
|
|
3126
|
+
const frontmatter = /^---\n[\s\S]*?\n---\n/.exec(text)
|
|
3127
|
+
if (frontmatter === null) return `${block}\n\n${text}`
|
|
3128
|
+
const end = frontmatter[0].length
|
|
3129
|
+
return `${text.slice(0, end)}\n${block}\n${text.slice(end)}`
|
|
3130
|
+
}
|
|
3131
|
+
|
|
3132
|
+
/**
|
|
3133
|
+
* Reconcile every rule file's coverage block against SURFACES.
|
|
3134
|
+
*
|
|
3135
|
+
* `'write'` refreshes them; `'check'` is the CI gate. A rule file with NO block yet is REPORTED,
|
|
3136
|
+
* never failed — the blocks are inserted by the agent-layer wave that rewrites those files, and a
|
|
3137
|
+
* gate that failed before they exist would have to be landed disabled, which is how a gate stays
|
|
3138
|
+
* disabled. A block that exists and DISAGREES is a hard failure: that is drift, and drift in a
|
|
3139
|
+
* coverage claim is the defect the block was added for.
|
|
3140
|
+
*/
|
|
3141
|
+
export function reconcileCoverageBlocks(
|
|
3142
|
+
pkgRoot: string,
|
|
3143
|
+
mode: 'write' | 'check',
|
|
3144
|
+
): { failures: string[]; notes: string[] } {
|
|
3145
|
+
const failures: string[] = []
|
|
3146
|
+
const notes: string[] = []
|
|
3147
|
+
for (const rule of RULE_NAMES) {
|
|
3148
|
+
const rel = `agent/rules/basalt-${rule}.md`
|
|
3149
|
+
const path = resolve(pkgRoot, rel)
|
|
3150
|
+
if (!existsSync(path)) continue // assertion 2 already reports a missing rule file
|
|
3151
|
+
const text = readFileSync(path, 'utf8')
|
|
3152
|
+
const current = readCoverageBlock(text)
|
|
3153
|
+
const expected = coverageBlock(rule)
|
|
3154
|
+
if (mode === 'check') {
|
|
3155
|
+
if (current === null) notes.push(`${rel}: no <!-- basalt:coverage --> block yet`)
|
|
3156
|
+
else if (current !== expected) {
|
|
3157
|
+
failures.push(
|
|
3158
|
+
`${rel}: coverage block is out of sync with SURFACES (check-coverage --write)`,
|
|
3159
|
+
)
|
|
3160
|
+
}
|
|
3161
|
+
continue
|
|
3162
|
+
}
|
|
3163
|
+
if (current === expected) {
|
|
3164
|
+
notes.push(`${rel}: unchanged`)
|
|
3165
|
+
continue
|
|
3166
|
+
}
|
|
3167
|
+
writeFileSync(path, applyCoverageBlock(text, expected), 'utf8')
|
|
3168
|
+
notes.push(`${rel}: ${current === null ? 'block inserted' : 'block updated'}`)
|
|
3169
|
+
}
|
|
3170
|
+
return { failures, notes }
|
|
3171
|
+
}
|
|
3172
|
+
|
|
3173
|
+
// ──────────────────────────────────────────────────────────────────────────────
|
|
3174
|
+
// check-coverage — 10-assertion coverage gate
|
|
2914
3175
|
// ──────────────────────────────────────────────────────────────────────────────
|
|
2915
3176
|
|
|
2916
3177
|
/**
|
|
2917
|
-
*
|
|
2918
|
-
* Returns 0 when all pass; 1 when any fail (console.error each failure).
|
|
3178
|
+
* Per-file line budgets for the shipped agent layer (`docs/CONTROLS-SPEC.md` §7's target table).
|
|
2919
3179
|
*
|
|
2920
|
-
*
|
|
3180
|
+
* A budget, not a guideline: seven `src/**`-scoped rules loaded ~2,000 lines on any source edit
|
|
3181
|
+
* before the merge, and 55% of that text was doctrine nothing enforced. The numbers are the bar the
|
|
3182
|
+
* spec argued, so they live beside the gate that reads them rather than in prose.
|
|
3183
|
+
*/
|
|
3184
|
+
const AGENT_LINE_BUDGETS: Readonly<Record<string, number>> = {
|
|
3185
|
+
'agent/rules/basalt-tokens.md': 160,
|
|
3186
|
+
'agent/rules/basalt-mantine.md': 180,
|
|
3187
|
+
'agent/rules/basalt-charts.md': 140,
|
|
3188
|
+
'agent/rules/basalt-state.md': 160,
|
|
3189
|
+
'agent/rules/basalt-controls.md': 160,
|
|
3190
|
+
'agent/rules/basalt-batteries.md': 220,
|
|
3191
|
+
'agent/skills/basalt-app/SKILL.md': 100,
|
|
3192
|
+
'agent/skills/basalt-design/SKILL.md': 100,
|
|
3193
|
+
'agent/skills/basalt-charts/SKILL.md': 100,
|
|
3194
|
+
'agent/templates/CLAUDE-block.md.tpl': 40,
|
|
3195
|
+
'agent/templates/DESIGN.md.tpl': 45,
|
|
3196
|
+
}
|
|
3197
|
+
|
|
3198
|
+
/** The whole-layer bar — a per-file budget alone can be satisfied by adding a seventh rule file. */
|
|
3199
|
+
const AGENT_RULE_TOTAL_BUDGET = 1050
|
|
3200
|
+
|
|
3201
|
+
/**
|
|
3202
|
+
* Assert the 11 invariants against the live SURFACES + GUARD_RULES + the shipped agent layer, and
|
|
3203
|
+
* (with `--write` / `--check`) reconcile the generated `<!-- basalt:coverage -->` header of every
|
|
3204
|
+
* rule file against SURFACES. Returns 0 when all pass; 1 when any fail (console.error each failure).
|
|
3205
|
+
*
|
|
3206
|
+
* Eleven assertions:
|
|
2921
3207
|
* 1. Every doctrine spec's guardKinds ⊆ keyof GUARD_RULES.
|
|
2922
3208
|
* 2. Every doctrine rule (deduped) maps to agent/rules/basalt-{rule}.md on disk.
|
|
2923
3209
|
* 3. Every doctrine skill (deduped) maps to agent/skills/{skill}/SKILL.md on disk.
|
|
@@ -2931,14 +3217,29 @@ export function sync(opts: SyncOptions = {}, invocationCwd: string = process.cwd
|
|
|
2931
3217
|
* the assertion's own comment below for why); basalt's own CI does
|
|
2932
3218
|
* (`tests/surfaces-coverage.test.ts`).
|
|
2933
3219
|
* 8. Every doctrine optionalPeers entry exists in peerDependencies AND peerDependenciesMeta.
|
|
3220
|
+
* 9. Every registered oxlint plugin rule (PLUGIN_RULE_ID_LIST) maps to EXACTLY ONE surface's
|
|
3221
|
+
* `pluginRules` — so a new rule cannot ship homeless, and one rule cannot be counted twice as
|
|
3222
|
+
* coverage. Like assertion 7 this reads SURFACES only: whether the plugin really registers
|
|
3223
|
+
* those ids is asserted against the plugin itself in `configs/oxlint-plugin.test.ts`, which a
|
|
3224
|
+
* shipped CLI running from a consumer's node_modules cannot do.
|
|
3225
|
+
* 10. Every shipped agent-layer file is inside its line budget, and the six rules are inside the
|
|
3226
|
+
* whole-layer total (`AGENT_LINE_BUDGETS` / `AGENT_RULE_TOTAL_BUDGET`).
|
|
3227
|
+
* 11. Every `GUARD_RULES` kind appears on at least one doctrine surface — assertion 9's twin for
|
|
3228
|
+
* the text lane. Without it a kind could ship, be documented as a law in the spec, and be
|
|
3229
|
+
* omitted from every generated coverage header, which is how two wave-6 kinds left
|
|
3230
|
+
* `agent/rules/basalt-controls.md` printing "guard kinds — none" (D8). Unlike 9 this is
|
|
3231
|
+
* one-directional: a kind may legitimately appear on SEVERAL surfaces (`raw-hex` is on
|
|
3232
|
+
* `./charts` and `./tokens`), because a text scan is not partitioned by subpath the way a
|
|
3233
|
+
* plugin rule id is.
|
|
2934
3234
|
*
|
|
2935
3235
|
* Tooling surfaces are exempt from assertions 1–3 by the discriminant.
|
|
2936
3236
|
* Synthetic #-keys participate in assertions 1 and 2 but feed assertion 3 only
|
|
2937
3237
|
* via the deduped skill union (no independent per-#-surface skill row).
|
|
2938
3238
|
*/
|
|
2939
|
-
export function checkCoverage(): number {
|
|
3239
|
+
export function checkCoverage(flags: readonly string[] = []): number {
|
|
2940
3240
|
const pkgRoot = packageRoot()
|
|
2941
3241
|
const failures: string[] = []
|
|
3242
|
+
const notes: string[] = []
|
|
2942
3243
|
|
|
2943
3244
|
const allSpecs = Object.entries(SURFACES) as [string, SurfaceSpec][]
|
|
2944
3245
|
const doctrineSpecs = (Object.values(SURFACES) as SurfaceSpec[]).filter(
|
|
@@ -3082,9 +3383,83 @@ export function checkCoverage(): number {
|
|
|
3082
3383
|
}
|
|
3083
3384
|
}
|
|
3084
3385
|
|
|
3386
|
+
// ── Assertion 9: every plugin rule maps to exactly one surface ───────────────
|
|
3387
|
+
// The plugin's own rules were the one half of the enforcement seam SURFACES did not model, so a
|
|
3388
|
+
// generated coverage header could name a guard kind and stay silent about the AST rule doing the
|
|
3389
|
+
// actual work (D8). Now every id has exactly one home, and "exactly" is both directions: an
|
|
3390
|
+
// unmapped id is a rule no doc claims, a doubly-mapped one is coverage counted twice.
|
|
3391
|
+
const surfacesPerPluginRule = new Map<string, string[]>()
|
|
3392
|
+
for (const [key, spec] of allSpecs) {
|
|
3393
|
+
if (spec.kind !== 'doctrine') continue
|
|
3394
|
+
for (const id of spec.pluginRules) {
|
|
3395
|
+
surfacesPerPluginRule.set(id, [...(surfacesPerPluginRule.get(id) ?? []), key])
|
|
3396
|
+
}
|
|
3397
|
+
}
|
|
3398
|
+
for (const id of PLUGIN_RULE_ID_LIST) {
|
|
3399
|
+
const owners = surfacesPerPluginRule.get(id) ?? []
|
|
3400
|
+
if (owners.length === 1) continue
|
|
3401
|
+
failures.push(
|
|
3402
|
+
owners.length === 0
|
|
3403
|
+
? `Plugin rule 'basalt/${id}' maps to no surface — add it to one surface's pluginRules`
|
|
3404
|
+
: `Plugin rule 'basalt/${id}' maps to ${owners.length} surfaces (${owners.join(', ')}) — exactly one`,
|
|
3405
|
+
)
|
|
3406
|
+
}
|
|
3407
|
+
|
|
3408
|
+
// ── Assertion 10: the agent layer stays inside its line budgets ─────────────
|
|
3409
|
+
// The budgets ARE the doctrine (docs/CONTROLS-SPEC.md §7): thirteen rules reached 4,177 lines by
|
|
3410
|
+
// growing a few lines at a time, each addition locally reasonable, and the only thing that
|
|
3411
|
+
// notices is a number nobody is checking. A rule over budget is over budget in the commit that
|
|
3412
|
+
// did it, not four minors later.
|
|
3413
|
+
for (const [rel, budget] of Object.entries(AGENT_LINE_BUDGETS)) {
|
|
3414
|
+
const abs = resolve(pkgRoot, rel)
|
|
3415
|
+
if (!existsSync(abs)) continue // assertions 2/3 already report a missing rule or skill
|
|
3416
|
+
const lines = readFileSync(abs, 'utf8').split('\n').length - 1
|
|
3417
|
+
if (lines > budget) failures.push(`${rel}: ${lines} lines exceeds its ${budget}-line budget`)
|
|
3418
|
+
}
|
|
3419
|
+
const ruleTotal = RULE_NAMES.reduce((sum, rule) => {
|
|
3420
|
+
const abs = resolve(pkgRoot, `agent/rules/basalt-${rule}.md`)
|
|
3421
|
+
return existsSync(abs) ? sum + readFileSync(abs, 'utf8').split('\n').length - 1 : sum
|
|
3422
|
+
}, 0)
|
|
3423
|
+
if (ruleTotal > AGENT_RULE_TOTAL_BUDGET) {
|
|
3424
|
+
failures.push(
|
|
3425
|
+
`agent/rules/*.md: ${ruleTotal} lines total exceeds the ${AGENT_RULE_TOTAL_BUDGET}-line budget`,
|
|
3426
|
+
)
|
|
3427
|
+
}
|
|
3428
|
+
|
|
3429
|
+
// ── Assertion 11: every guard kind maps to at least one surface ──────────────
|
|
3430
|
+
// The guard-kind twin of assertion 9. Assertion 1 already checks the other direction (no surface
|
|
3431
|
+
// names a kind that does not exist); this one catches the kind nobody claims, whose coverage
|
|
3432
|
+
// header therefore under-reports the lane that actually enforces it.
|
|
3433
|
+
const surfacedGuardKinds = new Set<string>()
|
|
3434
|
+
for (const spec of doctrineSpecs) {
|
|
3435
|
+
for (const kind of spec.guardKinds) surfacedGuardKinds.add(kind)
|
|
3436
|
+
}
|
|
3437
|
+
for (const kind of Object.keys(GUARD_RULES)) {
|
|
3438
|
+
if (surfacedGuardKinds.has(kind)) continue
|
|
3439
|
+
failures.push(
|
|
3440
|
+
`Guard kind '${kind}' maps to no surface — add it to one surface's guardKinds so the ` +
|
|
3441
|
+
`generated coverage headers name it`,
|
|
3442
|
+
)
|
|
3443
|
+
}
|
|
3444
|
+
|
|
3445
|
+
// ── Coverage headers (--write / --check) ─────────────────────────────────────
|
|
3446
|
+
const write = flags.includes('--write')
|
|
3447
|
+
const check = flags.includes('--check')
|
|
3448
|
+
if (write && check) {
|
|
3449
|
+
failures.push('--write and --check are alternatives; pass one')
|
|
3450
|
+
} else if (write || check) {
|
|
3451
|
+
const result = reconcileCoverageBlocks(pkgRoot, write ? 'write' : 'check')
|
|
3452
|
+
failures.push(...result.failures)
|
|
3453
|
+
notes.push(...result.notes)
|
|
3454
|
+
}
|
|
3455
|
+
|
|
3085
3456
|
// ── Report ──────────────────────────────────────────────────────────────────
|
|
3457
|
+
for (const note of notes) {
|
|
3458
|
+
console.log(` ${note}`)
|
|
3459
|
+
}
|
|
3460
|
+
|
|
3086
3461
|
if (failures.length === 0) {
|
|
3087
|
-
console.log('✓ check-coverage: all
|
|
3462
|
+
console.log('✓ check-coverage: all 11 assertions pass.')
|
|
3088
3463
|
return 0
|
|
3089
3464
|
}
|
|
3090
3465
|
|
|
@@ -4596,7 +4971,7 @@ const COMMAND_FLAGS: Record<string, readonly string[]> = {
|
|
|
4596
4971
|
init: ['--with-router', '--with-query', '--merge-lint'],
|
|
4597
4972
|
sync: ['--force', '--check', '--tokens-only', '--framework'],
|
|
4598
4973
|
'check-theme': ['--audit-allows', '--tokens-only', '--framework'],
|
|
4599
|
-
'check-coverage': [],
|
|
4974
|
+
'check-coverage': ['--write', '--check'],
|
|
4600
4975
|
info: ['--json'],
|
|
4601
4976
|
doctor: ['--tokens-only', '--framework'],
|
|
4602
4977
|
'guard-hook': [],
|
|
@@ -4647,7 +5022,8 @@ function unknownFlag(cmd: string, flags: readonly string[]): string | null {
|
|
|
4647
5022
|
const USAGE =
|
|
4648
5023
|
'Usage: basalt-ui <--version | init [--with-router] [--with-query] [--merge-lint] |\n' +
|
|
4649
5024
|
' sync [--force] [--check] [--tokens-only|--framework] |\n' +
|
|
4650
|
-
' check-theme [--audit-allows] | check-coverage
|
|
5025
|
+
' check-theme [--audit-allows] | check-coverage [--write|--check] |\n' +
|
|
5026
|
+
' info [--json] |\n' +
|
|
4651
5027
|
' doctor [--tokens-only|--framework] | guard-hook | tokens:css | fonts:css | help>\n\n' +
|
|
4652
5028
|
'check-theme [--tokens-only|--framework] [--audit-allows]\n' +
|
|
4653
5029
|
' --audit-allows reports instead of scanning: every `theme-allow` annotation and every\n' +
|
|
@@ -4737,7 +5113,7 @@ export function run(argv: string[], cwd: string = process.cwd()): number | Promi
|
|
|
4737
5113
|
case 'check-theme':
|
|
4738
5114
|
return checkTheme(cwd, flags)
|
|
4739
5115
|
case 'check-coverage':
|
|
4740
|
-
return checkCoverage()
|
|
5116
|
+
return checkCoverage(flags)
|
|
4741
5117
|
case 'info':
|
|
4742
5118
|
return info(flags)
|
|
4743
5119
|
case 'doctor':
|
|
@@ -4,8 +4,10 @@
|
|
|
4
4
|
* - the legacy `oxfmt.json` → `.oxfmtrc.json` manifest/file migration
|
|
5
5
|
* - the `basalt doctor` CLI-vs-installed version-mismatch warning
|
|
6
6
|
* - the `basalt init` first-run hint to run `check-theme` and tune per-rule config
|
|
7
|
+
* - pruning managed rule/skill files a newer basalt-ui no longer ships
|
|
7
8
|
*/
|
|
8
9
|
import { afterEach, beforeEach, describe, expect, it } from 'bun:test'
|
|
10
|
+
import { createHash } from 'node:crypto'
|
|
9
11
|
import { existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs'
|
|
10
12
|
import { tmpdir } from 'node:os'
|
|
11
13
|
import { join, resolve } from 'node:path'
|
|
@@ -244,3 +246,98 @@ describe('seeds derive from basalt.roots', () => {
|
|
|
244
246
|
expect(readFileSync(resolve(dir, 'DESIGN.md'), 'utf8')).toContain('custom/p.ts')
|
|
245
247
|
})
|
|
246
248
|
})
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* Retired managed files — the 13→6 rule merge (`docs/CONTROLS-SPEC.md` §7) is the case this exists
|
|
252
|
+
* for: nine `.claude/rules/basalt-*.md` stopped shipping in one minor, and a consumer that never
|
|
253
|
+
* loses them goes on reading superseded doctrine with every gate green.
|
|
254
|
+
*/
|
|
255
|
+
describe('sync — retired rules and skills', () => {
|
|
256
|
+
const RETIRED_RULE = '.claude/rules/basalt-router.md'
|
|
257
|
+
const RETIRED_SKILL = '.claude/skills/basalt-legacy/SKILL.md'
|
|
258
|
+
|
|
259
|
+
/** Seed a real install, then plant a managed file this basalt version no longer ships. */
|
|
260
|
+
function plantRetired(dest: string, body: string, tracked = true): void {
|
|
261
|
+
writeFixture('package.json', JSON.stringify({ name: 'fixture' }))
|
|
262
|
+
capture(() => init(dir))
|
|
263
|
+
writeFixture(dest, body)
|
|
264
|
+
const manifestAbs = resolve(dir, MANIFEST_PATH)
|
|
265
|
+
const manifest = JSON.parse(readFileSync(manifestAbs, 'utf8')) as {
|
|
266
|
+
files: Record<string, string>
|
|
267
|
+
}
|
|
268
|
+
// The raw-hash ledger path — what a pre-normalization CLI recorded, and enough to prove
|
|
269
|
+
// "untouched since basalt wrote it" without reaching into the module's private normalizer.
|
|
270
|
+
if (tracked) manifest.files[dest] = createHash('sha256').update(body, 'utf8').digest('hex')
|
|
271
|
+
writeFixture(MANIFEST_PATH, `${JSON.stringify(manifest, null, 2)}\n`)
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
function manifestFiles(): Record<string, string> {
|
|
275
|
+
return (
|
|
276
|
+
JSON.parse(readFileSync(resolve(dir, MANIFEST_PATH), 'utf8')) as {
|
|
277
|
+
files: Record<string, string>
|
|
278
|
+
}
|
|
279
|
+
).files
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
it('deletes an untouched retired rule file and drops its manifest entry', () => {
|
|
283
|
+
plantRetired(RETIRED_RULE, '# superseded by basalt-state.md\n')
|
|
284
|
+
const { code, log } = capture(() => sync({}, dir))
|
|
285
|
+
expect(code).toBe(0)
|
|
286
|
+
expect(existsSync(resolve(dir, RETIRED_RULE))).toBe(false)
|
|
287
|
+
expect(manifestFiles()[RETIRED_RULE]).toBeUndefined()
|
|
288
|
+
expect(log).toContain(RETIRED_RULE)
|
|
289
|
+
// The six live rules are untouched by the prune.
|
|
290
|
+
expect(existsSync(resolve(dir, '.claude/rules/basalt-state.md'))).toBe(true)
|
|
291
|
+
})
|
|
292
|
+
|
|
293
|
+
it('deletes a retired skill and removes the now-empty skill directory', () => {
|
|
294
|
+
plantRetired(RETIRED_SKILL, '---\nname: basalt-legacy\n---\n')
|
|
295
|
+
capture(() => sync({}, dir))
|
|
296
|
+
expect(existsSync(resolve(dir, RETIRED_SKILL))).toBe(false)
|
|
297
|
+
expect(existsSync(resolve(dir, '.claude/skills/basalt-legacy'))).toBe(false)
|
|
298
|
+
expect(existsSync(resolve(dir, '.claude/skills/basalt-app/SKILL.md'))).toBe(true)
|
|
299
|
+
})
|
|
300
|
+
|
|
301
|
+
it('leaves a locally-edited retired rule in place, keeps its entry, and says so', () => {
|
|
302
|
+
plantRetired(RETIRED_RULE, '# shipped body\n')
|
|
303
|
+
writeFixture(RETIRED_RULE, '# shipped body\n\nplus a local paragraph nobody else wrote\n')
|
|
304
|
+
const { code, log } = capture(() => sync({}, dir))
|
|
305
|
+
expect(code).toBe(0)
|
|
306
|
+
expect(existsSync(resolve(dir, RETIRED_RULE))).toBe(true)
|
|
307
|
+
expect(manifestFiles()[RETIRED_RULE]).toBeDefined()
|
|
308
|
+
expect(log).toContain('locally edited')
|
|
309
|
+
})
|
|
310
|
+
|
|
311
|
+
it('--force deletes a locally-edited retired rule', () => {
|
|
312
|
+
plantRetired(RETIRED_RULE, '# shipped body\n')
|
|
313
|
+
writeFixture(RETIRED_RULE, '# shipped body\n\nlocal edit\n')
|
|
314
|
+
capture(() => sync({ force: true }, dir))
|
|
315
|
+
expect(existsSync(resolve(dir, RETIRED_RULE))).toBe(false)
|
|
316
|
+
expect(manifestFiles()[RETIRED_RULE]).toBeUndefined()
|
|
317
|
+
})
|
|
318
|
+
|
|
319
|
+
it('--check reports a retired rule, exits 1, and writes nothing', () => {
|
|
320
|
+
plantRetired(RETIRED_RULE, '# superseded\n')
|
|
321
|
+
const { code, log } = capture(() => sync({ check: true }, dir))
|
|
322
|
+
expect(code).toBe(1)
|
|
323
|
+
expect(log).toContain('retired rule/skill file(s) still present')
|
|
324
|
+
expect(existsSync(resolve(dir, RETIRED_RULE))).toBe(true)
|
|
325
|
+
expect(manifestFiles()[RETIRED_RULE]).toBeDefined()
|
|
326
|
+
})
|
|
327
|
+
|
|
328
|
+
it('never prunes a tracked dest outside the rules/skills namespaces', () => {
|
|
329
|
+
// `src/routes/__root.tsx` is legitimately absent from `managedFiles()` whenever the router or
|
|
330
|
+
// query peer is missing — pruning on "this run did not place it" would delete a real scaffold.
|
|
331
|
+
plantRetired('src/routes/__root.tsx', 'export const Route = null\n')
|
|
332
|
+
capture(() => sync({}, dir))
|
|
333
|
+
expect(existsSync(resolve(dir, 'src/routes/__root.tsx'))).toBe(true)
|
|
334
|
+
expect(manifestFiles()['src/routes/__root.tsx']).toBeDefined()
|
|
335
|
+
})
|
|
336
|
+
|
|
337
|
+
it('init prunes a retired rule too', () => {
|
|
338
|
+
plantRetired(RETIRED_RULE, '# superseded\n')
|
|
339
|
+
const { log } = capture(() => init(dir))
|
|
340
|
+
expect(existsSync(resolve(dir, RETIRED_RULE))).toBe(false)
|
|
341
|
+
expect(log).toContain('no longer ship')
|
|
342
|
+
})
|
|
343
|
+
})
|