@hegemonart/get-design-done 1.38.5 → 1.39.2
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/.claude-plugin/marketplace.json +2 -2
- package/.claude-plugin/plugin.json +1 -1
- package/CHANGELOG.md +51 -0
- package/README.md +8 -0
- package/SKILL.md +2 -0
- package/agents/cost-forecaster.md +91 -0
- package/agents/design-verifier.md +1 -1
- package/agents/ds-migration-planner.md +72 -0
- package/hooks/budget-enforcer.ts +146 -0
- package/package.json +1 -1
- package/reference/cost-governance.md +93 -0
- package/reference/migrations/material-3-to-4.md +53 -0
- package/reference/migrations/mui-v6.md +58 -0
- package/reference/migrations/shadcn-v2.md +77 -0
- package/reference/migrations/tailwind-v4.md +73 -0
- package/reference/registry.json +35 -0
- package/reference/schemas/budget.schema.json +10 -0
- package/reference/schemas/events.schema.json +1 -1
- package/reference/schemas/generated.d.ts +94 -1
- package/scripts/lib/budget/cost-forecast.cjs +103 -0
- package/scripts/lib/budget/project-cap.cjs +55 -0
- package/scripts/lib/budget/roi.cjs +73 -0
- package/scripts/lib/migration/codemod-gen.cjs +74 -0
- package/skills/budget/SKILL.md +45 -0
- package/skills/roi/SKILL.md +54 -0
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# Material Design Token Migration (M3 → next) — Rule Library
|
|
2
|
+
|
|
3
|
+
Honest framing: Google has **not** released a "Material Design 4" spec — Material publicly evolved M2 → M3 (Material You), and there is no public M4. This library is therefore a **forward-looking rule set for migrating a Material-token-based design system to the next Material major**, grounded entirely in the **real, well-documented M2 → M3 token migration** (the `--mdc-*` → `--md-sys-*` reshape, color-role expansion, reference-vs-system token split, typescale rename, and `mwc-*` → `md-*` component rename). Those concrete patterns are the migration *shape* the next major will most plausibly follow; nothing below invents tokens from a fictional spec. GDD never auto-applies — these rules feed `ds-migration-planner` and template `codemod-gen.cjs` output that the USER reviews and runs.
|
|
4
|
+
|
|
5
|
+
## Detection
|
|
6
|
+
|
|
7
|
+
Detect a Material-token design system from `package.json` **only** (no lockfile, no source scan at this stage):
|
|
8
|
+
|
|
9
|
+
- **Dependencies** (`dependencies` / `devDependencies` / `peerDependencies`):
|
|
10
|
+
- `@material/web` — Material Web Components (M3-era; the forward-migration target surface).
|
|
11
|
+
- `@material/*` MDC packages — `@material/button`, `@material/textfield`, `@material/theme`, `@material/typography`, etc. (M2-era MDC Web). Presence of many `@material/<component>` entries strongly signals an M2 MDC system.
|
|
12
|
+
- `material-components-web` — the MDC Web umbrella package (M2-era).
|
|
13
|
+
- `@angular/material` — read the **major** version: `<= 14` ≈ M2 theming API, `>= 15` ≈ M3 / MDC-backed components, `>= 17` ≈ M3 tokens default. Pair with `@angular/cdk` major as a cross-check.
|
|
14
|
+
- **Signal strength**: treat as a migration candidate when (a) at least one package above is present **and** (b) the manifest shows Material token usage intent — e.g. a `@material/theme` / `@material/tokens` dep, a `"material"` config block, or MDC/M3 packages pinned to a pre-target major. Manifest-only detection — defer actual token-occurrence scanning to the planner's codemod-gen pass.
|
|
15
|
+
|
|
16
|
+
## Migration rules
|
|
17
|
+
|
|
18
|
+
| Rule ID | Kind | From → To | Note |
|
|
19
|
+
|---------|------|-----------|------|
|
|
20
|
+
| MD-01 | token-rename | `--mdc-theme-primary` → `--md-sys-color-primary` | Core M2→M3 theme→system color reshape; root of most edits. |
|
|
21
|
+
| MD-02 | token-rename | `--mdc-theme-on-primary` → `--md-sys-color-on-primary` | "on-" foreground roles move under `md.sys.color`. |
|
|
22
|
+
| MD-03 | token-rename | `--mdc-theme-secondary` / `--mdc-theme-on-secondary` → `--md-sys-color-secondary` / `--md-sys-color-on-secondary` | Secondary role pair preserved across majors. |
|
|
23
|
+
| MD-04 | token-rename | `--mdc-theme-surface` → `--md-sys-color-surface` | Surface base role; precondition for the surface-container set (MD-05). |
|
|
24
|
+
| MD-05 | new-default | *(no M2 equivalent)* → `--md-sys-color-surface-container` / `-surface-container-high` / `-surface-container-highest` / `-surface-container-low` / `-surface-container-lowest` | M3 tonal surface-container roles; replace ad-hoc elevation overlays. High visual delta. |
|
|
25
|
+
| MD-06 | token-rename | `--mdc-theme-outline` (or hand-rolled border token) → `--md-sys-color-outline` | Outline promoted to a first-class system color role. |
|
|
26
|
+
| MD-07 | new-default | *(no M2 equivalent)* → `--md-sys-color-outline-variant` | Low-emphasis dividers/borders; previously a faded outline or custom value. |
|
|
27
|
+
| MD-08 | token-rename | `md.ref.palette.primary40` (reference tokens) → `md.sys.color.primary` (system tokens) | Enforce ref→sys indirection: components consume `md.sys.*`, never `md.ref.palette.*` directly. |
|
|
28
|
+
| MD-09 | token-rename | `--mdc-typography-headline6-*` → `--md-sys-typescale-title-large-*` | Typescale rename + remap (M2 named sizes → M3 role-based scale). Verify per-role size/weight mapping. |
|
|
29
|
+
| MD-10 | token-rename | `--mdc-typography-body1-*` / `body2-*` → `--md-sys-typescale-body-large-*` / `body-medium-*` | Body typescale remap; line-height and tracking values also shift. |
|
|
30
|
+
| MD-11 | token-rename | `--mdc-elevation-*` / elevation z-values → `--md-sys-elevation-level0..level5` | Numeric dp elevations become a 6-step token scale; pair with surface-container roles. |
|
|
31
|
+
| MD-12 | token-rename | `--mdc-shape-medium` / corner radii → `--md-sys-shape-corner-medium` (`-small` / `-large` / `-extra-large` / `-full`) | Shape scale renamed and bucketed into corner roles. |
|
|
32
|
+
| MD-13 | new-default | *(opt-in)* → dynamic color enabled (`md.sys.color.*` derived from a seed/source color) | M3 default theming model; replaces a static brand palette. Highest visual delta. |
|
|
33
|
+
| MD-14 | remove-component | `mwc-button` → `md-filled-button` (and `md-outlined-button` / `md-text-button` / `md-tonal-button`) | `@material/web` element rename; one MWC element fans out to explicit M3 variants — no clean 1:1 transform, pick a variant by hand (manual TODO). |
|
|
34
|
+
| MD-15 | remove-component | `mwc-textfield` → `md-filled-text-field` / `md-outlined-text-field` | `mwc-*` → `md-*` rename; one element fans out to 2 variants — appearance now encoded in the element name, choose by hand (manual TODO). |
|
|
35
|
+
| MD-16 | rename-prop | `?outlined` / `?raised` boolean attrs → variant element (per MD-14/MD-15) | Style-toggle props removed; pick the right `md-*` element instead of toggling a flag. |
|
|
36
|
+
| MD-17 | remove-component | `mwc-icon-button-toggle` → *(removed)* — compose `md-icon-button` + `toggle` selected state | Dedicated toggle element dropped; rebuild with base icon-button + selected state. |
|
|
37
|
+
| MD-18 | rename-class | `.mdc-button` / `.mdc-card` (BEM CSS classes) → web-component element + part styling | M3 Web moves from BEM-class theming to custom-element `::part()` / token styling. |
|
|
38
|
+
|
|
39
|
+
## Impact notes
|
|
40
|
+
|
|
41
|
+
**High visual delta — design + QA review required, do not ship blind:**
|
|
42
|
+
- **Dynamic color (MD-13)**: switching from a fixed brand palette to seed-derived `md.sys.color.*` changes nearly every on-screen color. Confirm seed source, contrast pairs (`on-*` roles), and light/dark tonal output before adopting.
|
|
43
|
+
- **Surface-container roles (MD-05, MD-07)**: replacing elevation-overlay surfaces with tonal containers visibly alters card/sheet/menu backgrounds and divider weight. Audit layering after migration.
|
|
44
|
+
- **Typescale (MD-09, MD-10)**: M2 named styles do not map 1:1 to M3 role sizes — size, weight, line-height, and tracking all move. Expect reflow; review dense text and truncation.
|
|
45
|
+
|
|
46
|
+
**Mechanical — codemod-templatable, low judgment per occurrence:**
|
|
47
|
+
- Pure custom-property renames (MD-01–MD-04, MD-06, MD-08, MD-11, MD-12) are string-substitution-shaped. `codemod-gen.cjs` can template these as find/replace with a token map; still diff-review for false positives in comments, strings, and unrelated `--mdc-*` namespaces.
|
|
48
|
+
|
|
49
|
+
**Manual-review items — never auto-apply:**
|
|
50
|
+
- **Component renames + fan-out (MD-14, MD-15)**: one source element maps to several target variants; the correct target depends on the original prop set — a human (or planner) must choose. Generated templates should emit a *candidate* mapping plus a TODO, not a committed rename.
|
|
51
|
+
- **Removed components (MD-17)** and **prop→variant collapse (MD-16)**: require reconstructing behavior (selected/toggle state, layout) — flag for hand-editing.
|
|
52
|
+
- **Class→element migration (MD-18)**: BEM-class theming → custom-element `::part()` is a structural rewrite, not a rename; scope as its own task.
|
|
53
|
+
- **Angular major bumps**: when detection keys off `@angular/material`, the framework upgrade (DI, theming mixins, component API) dwarfs the token rename — sequence the framework migration first, tokens second.
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# MUI (Material UI) v5 → v6 — Migration Rule Library
|
|
2
|
+
|
|
3
|
+
A proposal-only rule library that `agents/ds-migration-planner.md` reads to draft a v5→v6 migration plan, and that the pure `scripts/lib/migration/codemod-gen.cjs` uses to template codemods the USER reviews and runs. GDD never auto-applies any rule here — every entry is a *proposal* with a human in the loop. v6 is mostly mechanical (import + prefix renames) with two high-visual-delta areas: the `Grid` overhaul and the CSS-theme-variables adoption.
|
|
4
|
+
|
|
5
|
+
## Detection
|
|
6
|
+
|
|
7
|
+
Detect MUI and the v5→v6 boundary from `package.json` only — no lockfile read, no source scan. The planner classifies the project from the dependency graph alone:
|
|
8
|
+
|
|
9
|
+
- **Is MUI?** `dependencies` / `devDependencies` contains `@mui/material`. Treat the presence of any of these companion packages as corroborating evidence: `@mui/system`, `@mui/icons-material`, `@mui/lab`, `@mui/styled-engine` (or the styled-components variant `@mui/styled-engine-sc`), `@mui/base`.
|
|
10
|
+
- **On v5 (migration applies)?** The `@mui/material` semver range resolves to a `^5` / `5.x` major. Treat `~5`, `>=5 <6`, and a pinned `5.x.y` the same way. A `^6` / `6.x` range means the project is already on v6 — emit no rules. A `^7` range means it has moved past the scope of this library (defer to a v6→v7 library).
|
|
11
|
+
- **Companion-major skew (flag, do not migrate):** if `@mui/material` is `^5` but `@mui/system` or `@mui/styled-engine` resolves to `^6` (or vice-versa), the install is half-upgraded. Surface this as a blocker for the planner — every rule below assumes a coherent v5 baseline, and a mixed graph produces unreliable codemod output.
|
|
12
|
+
- **Peer majors to record** (planner notes, not rules in this library): `@mui/x-data-grid`, `@mui/x-date-pickers`, `@mui/x-charts`, `@mui/x-tree-view`. MUI X tracks its own v5→v6/v7 line on a separate cadence, but a Core v6 bump usually forces an X major bump too. Flag the Core/X pair so the planner sequences both migrations together rather than leaving the graph inconsistent.
|
|
13
|
+
- **Styled engine:** record whether `@mui/styled-engine-sc` (styled-components) is present — it changes how MUI6-19 (`styled`/`deepmerge`) is reviewed versus the default Emotion engine.
|
|
14
|
+
- **Runtime gates:** read `engines.node` and the `typescript` devDependency. v6 requires Node 14+ and TypeScript 4.7+ and drops IE 11 (see MUI6-17); a lower floor in `package.json` is itself a migration task.
|
|
15
|
+
|
|
16
|
+
## Migration rules
|
|
17
|
+
|
|
18
|
+
Kinds are constrained to: `rename-class`, `rename-prop`, `remove-component`, `token-rename`, `new-default`.
|
|
19
|
+
|
|
20
|
+
| Rule ID | Kind | From → To | Note |
|
|
21
|
+
|---|---|---|---|
|
|
22
|
+
| MUI6-01 | rename-class | `import { Unstable_Grid2 as Grid2 } from '@mui/material'` → `import { Grid2 } from '@mui/material'` | The `Unstable_` prefix is dropped; `Grid2` is now stable and is slated to become the default `Grid` in the next major. |
|
|
23
|
+
| MUI6-02 | rename-prop | `<Grid item xs={12} sm={6}>` → `<Grid size={{ xs: 12, sm: 6 }}>` | Grid2's per-breakpoint props collapse into one object `size` prop; the standalone `item` prop is gone (every non-container is implicitly an item). |
|
|
24
|
+
| MUI6-03 | rename-prop | `<Grid xs={6}>` → `<Grid size={6}>` | When the value is identical across breakpoints, `size` takes a single number instead of an object — shorthand for the MUI6-02 object form. |
|
|
25
|
+
| MUI6-04 | rename-prop | `xsOffset={2} smOffset={3}` → `offset={{ xs: 2, sm: 3 }}` | Per-breakpoint `*Offset` props collapse into one object `offset` prop (single-value form `offset={2}` also allowed). |
|
|
26
|
+
| MUI6-05 | new-default | `<Grid xs>` (boolean auto-grow) → `<Grid size="grow">` | The boolean `true` size value is renamed to the string `"grow"`; a bare boolean breakpoint prop no longer compiles. |
|
|
27
|
+
| MUI6-06 | new-default | `<Grid2 disableEqualOverflow>` → (prop removed) | `disableEqualOverflow` is deleted; v6 Grid2 lays out spacing with CSS `gap`, so the old negative-margin overflow workaround is unnecessary. |
|
|
28
|
+
| MUI6-07 | rename-class | `experimental_extendTheme` → `extendTheme` (from `@mui/material/styles`) | The CSS-vars theme factory leaves experimental status; import the stable name. The `experimental_` alias is removed, not deprecated-with-shim. |
|
|
29
|
+
| MUI6-08 | rename-class | `Experimental_CssVarsProvider as CssVarsProvider` → `CssVarsProvider` | Drop the `Experimental_` prefix. Preferred end-state: v6 `ThemeProvider` now subsumes every CssVarsProvider feature, so consolidating onto a single `ThemeProvider` is the recommended target. |
|
|
30
|
+
| MUI6-09 | new-default | `createTheme({ ... })` → `createTheme({ cssVariables: true, ... })` | Opt-in flag that emits CSS custom properties for the theme; required to read `theme.vars.*` and to get SSR-safe, flash-free dark mode. Off by default — adding it is a proposal, never automatic. |
|
|
31
|
+
| MUI6-10 | token-rename | `theme.palette.primary.main` (in `styled`/`sx`) → `theme.vars.palette.primary.main` | Under `cssVariables: true`, read palette tokens through `theme.vars.*` (CSS-var references) so values resolve per color scheme at runtime instead of being baked in at build time. Plain `theme.palette.*` still returns a value but won't switch schemes via CSS. |
|
|
32
|
+
| MUI6-11 | token-rename | `theme.palette.mode === 'dark' ? a : b` → `theme.applyStyles('dark', { ... })` | Replace color-mode ternaries in `styled`/`sx` with `applyStyles`; ternaries break under CSS-vars dark mode because both schemes are emitted into one stylesheet and the JS branch resolves only once. |
|
|
33
|
+
| MUI6-12 | remove-component | `import { LoadingButton } from '@mui/lab'` → `import { Button } from '@mui/material'` + `loading` / `loadingPosition` props | `LoadingButton` is merged into the core `Button`; the `@mui/lab` export is deprecated. Move `loading`, `loadingPosition`, and `loadingIndicator` onto `Button`. |
|
|
34
|
+
| MUI6-13 | remove-component | `<ListItem button autoFocus disabled selected>` → `<ListItem disablePadding><ListItemButton autoFocus disabled selected>…</ListItemButton></ListItem>` | The interactive `button` behavior and its `autoFocus` / `disabled` / `selected` props are removed from `ListItem`; wrap the content in `ListItemButton`. |
|
|
35
|
+
| MUI6-14 | rename-class | `listItemClasses` (e.g. `.Mui-selected`, `.Mui-focusVisible` keys) → `listItemButtonClasses` | Companion to MUI6-13: the interactive state class keys move from the `ListItem` class object to `ListItemButton`, so any `styleOverrides` or CSS targeting those keys must be re-pointed. |
|
|
36
|
+
| MUI6-15 | new-default | `<Divider orientation="vertical" />` renders an `hr` element → renders a `div` | A vertical `Divider` now emits a `div` carrying a separator ARIA role (an `hr` cannot be vertical/inline). Update any CSS or test selector that assumed an `hr` element. |
|
|
37
|
+
| MUI6-16 | new-default | `AccordionSummary` content is bare → wrapped in an `h3` heading element | The summary is wrapped in a heading by default; override the level via `slotProps={{ heading: { component: 'h4' } }}`. Affects heading-order audits and heading-based selectors/queries. |
|
|
38
|
+
| MUI6-17 | new-default | Node 12 / TS 3.5 / IE 11 baseline → Node 14+, TS 4.7+, IE 11 dropped | Raise `engines.node`, bump the `typescript` devDependency, and remove IE-11 polyfills / transpile targets. Pure compatibility surface — no component code shape change. |
|
|
39
|
+
| MUI6-18 | remove-component | UMD bundle (`@mui/material/umd`, CDN `<script>` usage) → (removed) | The pre-bundled UMD build is dropped (~2.5 MB saved). Move CDN/`<script>` consumers to a bundler or native ESM; pure-ESM package `exports` are enforced, so deep internal `lib/` import paths also stop resolving. |
|
|
40
|
+
| MUI6-19 | token-rename | `import { deepmerge } from '@mui/utils'`; deep-spread of `theme.palette` in `styleOverrides` → re-verify against `@mui/utils` v6 | `styled` and `deepmerge` internals were reworked for the CSS-vars theme. Custom `styleOverrides` that mutated or deep-merged `theme.palette` should be re-tested (especially alongside MUI6-10). Flag for manual review; do not auto-rewrite — behavior, not just the import path, may differ. |
|
|
41
|
+
| MUI6-20 | rename-prop | `components={{ ... }}` / `componentsProps={{ ... }}` (on slotted components) → `slots={{ ... }}` / `slotProps={{ ... }}` | v6 finalizes the slot-API standardization: the legacy `components`/`componentsProps` pair is deprecated in favor of `slots`/`slotProps` across Modal, Popper, Autocomplete, Badge, Tooltip, and friends. Note that slot *names* are lowercased (e.g. `Backdrop` → `backdrop`). |
|
|
42
|
+
| MUI6-21 | remove-component | `import { Hidden } from '@mui/material'` → `sx` display utilities or `useMediaQuery` | The `Hidden` helper is removed; replace `<Hidden smDown>` with responsive `sx={{ display: { xs: 'none', sm: 'block' } }}` or a `useMediaQuery` guard. There is no drop-in component — this is a manual rewrite. |
|
|
43
|
+
| MUI6-22 | rename-class | `createMuiTheme` / `adaptV4Theme` (lingering v4 aliases) → `createTheme` | Any remaining v4-era `createMuiTheme` alias and the `adaptV4Theme` shim are removed in v6; collapse onto `createTheme`. Surfaces mainly in codebases that did a partial v4→v5 pass and left the aliases in place. |
|
|
44
|
+
|
|
45
|
+
## Impact notes
|
|
46
|
+
|
|
47
|
+
High visual delta — these require human review of the rendered result, not just a green diff:
|
|
48
|
+
|
|
49
|
+
- **Grid overhaul (MUI6-01…06):** moving off the `item xs={}` layout model to `size={{}}` *plus* the switch to CSS `gap` spacing can shift column widths, gutters, and wrap behavior. Generate the codemod proposal, then require a side-by-side visual diff before accepting. The official `v6.0.0/grid-v2-props` codemod covers the prop rename but not bespoke spacing/negative-margin math.
|
|
50
|
+
- **CSS theme variables (MUI6-09…11):** enabling `cssVariables: true` together with the `theme.vars.*` and `applyStyles` migration changes how *every* themed color resolves and how dark mode flips. This touches the whole surface at once — stage it behind a feature flag and review color and contrast holistically, not file-by-file.
|
|
51
|
+
- **Structural DOM rules (MUI6-13…16):** `ListItemButton` nesting, the vertical `Divider` → `div`, and the Accordion `h3` wrap all change the rendered DOM and heading order. Re-run accessibility and heading-order audits and fix brittle element/heading selectors in tests.
|
|
52
|
+
|
|
53
|
+
Mechanical — low risk, but still proposal-only (the USER reviews every generated patch):
|
|
54
|
+
|
|
55
|
+
- **Import / prefix / class renames** (MUI6-01, MUI6-07, MUI6-08, MUI6-12, MUI6-14) and the **engine bumps** (MUI6-17) are find-and-replace–class changes that `codemod-gen.cjs` can template safely. MUI6-18 is mechanical to detect but its *fix* (re-tooling a CDN consumer onto a bundler) is a build-system change, so route it to manual.
|
|
56
|
+
- **MUI6-19** looks mechanical (one import) but hides a behavior change — always tag it manual-review, never auto-apply.
|
|
57
|
+
|
|
58
|
+
Sequencing: do the prefix/import/class renames first (smallest blast radius), then the Grid pass, then the CSS-vars pass last (largest blast radius, and it interacts with MUI6-19). Resolve any companion-major skew and the peer MUI X majors surfaced in Detection *before* starting — a Core v6 bump typically drags an `@mui/x-*` major along with it.
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
# shadcn/ui v1 → v2 — Migration Rule Library
|
|
2
|
+
|
|
3
|
+
This library encodes the concrete, mechanical changes a project must make when moving a shadcn/ui codebase from the v1 era (Tailwind v3, HSL CSS variables, Radix `forwardRef`) to the v2 era (Tailwind v4 CSS-first theming, OKLCH variables, React 19 ref-as-prop). It is consumed by `agents/ds-migration-planner.md` to scope a migration and by `scripts/lib/migration/codemod-gen.cjs` to emit jscodeshift / ast-grep templates.
|
|
4
|
+
|
|
5
|
+
Every rule below is **proposal-only**. GDD never auto-applies a migration: the codemod generator emits reviewable templates, and the user inspects and runs each one before it touches the working tree. Treat the rules as a checklist the planner reasons over, not an executable patch — the planner's job is to scope, order, and impact-score, not to mutate code.
|
|
6
|
+
|
|
7
|
+
## Detection
|
|
8
|
+
|
|
9
|
+
shadcn/ui is **CLI-managed copy-in source**, not a versioned runtime dependency — the component code lives in the repo (`components/ui/*`), so there is no `shadcn` line in `dependencies` to read a version from. The planner therefore infers the era from `package.json` plus the project's `components.json`.
|
|
10
|
+
|
|
11
|
+
Primary boundary signals, strongest first:
|
|
12
|
+
|
|
13
|
+
- **`tailwindcss` major in `package.json`** is the single strongest signal: `^3.x` → v1 era; `^4.x` → v2 era. Almost everything else follows from this.
|
|
14
|
+
- **`components.json` shape** — its presence confirms a shadcn project. A non-empty `tailwind.config` key (a path to a JS/TS config) signals v1; an empty `tailwind.config` string plus a `tailwind.css` pointing at a file that opens with `@import "tailwindcss"` signals v2.
|
|
15
|
+
- **`react` / `react-dom` major** — `^19` indicates the project can use ref-as-prop and is on the v2 track; `^18` means `forwardRef` shims in the component source are still load-bearing.
|
|
16
|
+
|
|
17
|
+
Supporting (corroborating, not decisive) signals:
|
|
18
|
+
|
|
19
|
+
- **`@radix-ui/*` versions** — v1 pins per-primitive packages (e.g. `@radix-ui/react-dialog`); late-v2 projects often consolidate onto the unified `radix-ui` package. Either pattern is acceptable; the version floor matters more than the package name.
|
|
20
|
+
- **Animation dep** — `tailwindcss-animate` present → v1; `tw-animate-css` → v2.
|
|
21
|
+
- **`tailwind-merge` major** — `^2` → v1; `^3` → v2.
|
|
22
|
+
- **Neutral deps** — `next-themes`, `lucide-react`, `class-variance-authority`, `clsx` appear in both eras and are **not** boundary signals.
|
|
23
|
+
|
|
24
|
+
Version detection is **from `package.json` only** — never execute the shadcn CLI, hit a network registry, or shell out to resolve versions. If `tailwindcss` is absent or unparseable, the planner reports "era undetermined" rather than guessing.
|
|
25
|
+
|
|
26
|
+
## Migration rules
|
|
27
|
+
|
|
28
|
+
Each row is one codemod target. `Kind` is drawn from the `codemod-gen.cjs` enum: `rename-class` | `rename-prop` | `remove-component` | `token-rename` | `new-default`. Rule IDs are stable — the planner references them in its impact report and the generator names emitted templates after them.
|
|
29
|
+
|
|
30
|
+
| Rule ID | Kind | From → To | Note |
|
|
31
|
+
|---|---|---|---|
|
|
32
|
+
| SHADCN-01 | new-default | `tailwind.config.{js,ts}` `theme.extend` colors → `@theme` block in the CSS entry (after `@import "tailwindcss"`) | Tailwind v4 is CSS-first: theme tokens move out of JS config into an `@theme` directive. Structural move; the JS config is largely retired (only kept for plugins/content edge cases). |
|
|
33
|
+
| SHADCN-02 | token-rename | HSL triplet vars `--background: 0 0% 100%` → OKLCH `--background: oklch(1 0 0)` | Default theme variables switch from space-separated HSL channels to complete `oklch()` values across `:root` and `.dark`. Color values change representation, not (intended) appearance. |
|
|
34
|
+
| SHADCN-03 | rename-class | `hsl(var(--token))` wrappers & channel-opacity `bg-background/[alpha]` hacks → direct `var(--token)` / `bg-background` | v1 wrapped every var in `hsl(var(--x))`; v2 stores complete color functions, so the `hsl(...)` wrapper and the channel-based opacity workaround are removed. |
|
|
35
|
+
| SHADCN-04 | new-default | hand-rolled `@layer base { :root { … } }` tokens → `@theme inline { --color-*: var(--*) }` mapping | v2 exposes semantic tokens to the utility namespace via `@theme inline`, bridging raw `--background` vars to the `--color-background` utility Tailwind generates classes from. |
|
|
36
|
+
| SHADCN-05 | rename-prop | `React.forwardRef<T, P>((props, ref) => …)` → ref-as-prop `({ ref, ...props }: P & { ref?: Ref<T> })` | React 19 passes `ref` as a normal prop. v2 component source drops `forwardRef`; the trailing `Component.displayName = …` assignments become unnecessary. Mechanical, but touches every primitive file. |
|
|
37
|
+
| SHADCN-06 | new-default | component part JSX → add `data-slot="<component-part>"` attribute on each rendered element | v2 tags every primitive part with a stable `data-slot` for styling/targeting (e.g. `data-slot="card-header"`). Net-new attribute across every component file; near-zero visual delta. |
|
|
38
|
+
| SHADCN-07 | remove-component | `components/ui/toast.tsx` + `hooks/use-toast.ts` + Radix `<Toaster/>` → `sonner` `<Toaster/>` + `toast` import | The Radix-based toast and the `useToast` hook are removed in favor of Sonner. Imports of `useToast` / `@/components/ui/use-toast` must be rewritten to `import { toast } from "sonner"`. Behavioral + public-API change. |
|
|
39
|
+
| SHADCN-08 | rename-prop | `useToast()` call sites `toast({ title, description, variant })` → sonner `toast(title, { description })` / `toast.error(…)` | Sonner's signature differs from the v1 hook object form; `variant: "destructive"` maps to `toast.error`. Every call site needs reshaping. Pairs with SHADCN-07. |
|
|
40
|
+
| SHADCN-09 | token-rename | `tailwindcss-animate` plugin + dep → `tw-animate-css` via `@import "tw-animate-css"` in the CSS entry | The animation-utilities package is renamed. Remove the old devDependency and its plugin entry, add the new CSS import. Utility class names (`animate-in`, `fade-in-0`, `zoom-in-95`) are preserved. |
|
|
41
|
+
| SHADCN-10 | new-default | `components.json` `"style": "default"` → `"style": "new-york"` | The `default` style is removed in v2; `new-york` is the only remaining style, so any re-added component pulls new-york source. Some spacing, border, and icon-size defaults shift — moderate visual delta. |
|
|
42
|
+
| SHADCN-11 | rename-class | paired `h-4 w-4`, `h-6 w-6` icon sizing → `size-4`, `size-6` | v2 source adopts the `size-*` shorthand (Tailwind ≥3.4). Safe equivalent of matched `h-*`/`w-*`; mechanical, no visual delta. Only collapse pairs where the two values are equal. |
|
|
43
|
+
| SHADCN-12 | rename-prop | `cn()` relying on `tailwind-merge@^2` → `^3` | `tailwind-merge` v3 changes some merge-conflict resolution internals. Bump the dependency and re-verify `cn()` output where custom classes overlap component defaults. The `lib/utils.ts` `cn` body itself is unchanged. |
|
|
44
|
+
| SHADCN-13 | remove-component | per-primitive `@radix-ui/react-*` imports → unified `radix-ui` namespace imports | Optional in v2: `import * as DialogPrimitive from "@radix-ui/react-dialog"` → `import { Dialog as DialogPrimitive } from "radix-ui"`. Removes many small deps; not required for correctness, so flag as opt-in only. |
|
|
45
|
+
| SHADCN-14 | rename-class | `focus:ring-2 ring-offset-2` focus styles → `focus-visible:ring-[3px] focus-visible:ring-ring/50` | v2 components standardize on `focus-visible` plus a softened ring (`ring/50`, 3px). Visible focus-state change — moderate visual delta, and accessibility-relevant, so it cannot be silently dropped. |
|
|
46
|
+
| SHADCN-15 | token-rename | add v2 chart + sidebar tokens (`--chart-1..5`, `--sidebar`, `--sidebar-foreground`, …) absent in v1 themes | v2 ships extra semantic variables. If the project uses the Chart or Sidebar components, the `@theme` / `:root` block must gain these tokens or those components render unstyled. No-op for projects that use neither. |
|
|
47
|
+
| SHADCN-16 | new-default | `tailwind.config` `darkMode: ["class"]` → CSS `@custom-variant dark (&:is(.dark *))` | v4 expresses the dark variant in CSS rather than JS config; the `darkMode` array key is dropped and replaced by a `@custom-variant` declaration in the stylesheet entry. |
|
|
48
|
+
|
|
49
|
+
## Impact notes
|
|
50
|
+
|
|
51
|
+
The planner scores each rule as **visual-delta × usage × tests** to order the migration and decide what needs human eyes before the user runs the codemod.
|
|
52
|
+
|
|
53
|
+
**High visual-delta — review with screenshots / visual diff:**
|
|
54
|
+
|
|
55
|
+
- SHADCN-02 + SHADCN-03 — the whole HSL→OKLCH color move. Intended to be visually neutral, but it rewrites every color value, so it must be *proven* neutral against a baseline, not assumed.
|
|
56
|
+
- SHADCN-10 — `default`→`new-york` shifts spacing, radius, and icon defaults across the component set.
|
|
57
|
+
- SHADCN-14 — the focus-ring restyle changes a visible interaction state.
|
|
58
|
+
- SHADCN-15 — missing chart/sidebar tokens make those components render wrong (unstyled), which reads as a large visual delta where they're used.
|
|
59
|
+
|
|
60
|
+
**High behavioral / API delta — manual review required:**
|
|
61
|
+
|
|
62
|
+
- SHADCN-07 + SHADCN-08 — toast → Sonner changes a public API and runtime behavior. Call-site reshaping cannot be fully mechanical (variant mapping, return values), so every notification flow should be re-tested by hand.
|
|
63
|
+
- SHADCN-12 — `tailwind-merge` v3 can silently change which class wins in a merge. Review anywhere custom classes are layered over component defaults.
|
|
64
|
+
|
|
65
|
+
**Structural, low visual-delta — codemod-friendly, light review:**
|
|
66
|
+
|
|
67
|
+
- SHADCN-01, SHADCN-04, SHADCN-16 relocate theming from JS config into CSS. Mechanical once the target `@theme` shape is fixed, but they touch the build entry — a single broken import fails the whole stylesheet, so verify the app boots rather than diffing pixels.
|
|
68
|
+
|
|
69
|
+
**Purely mechanical — auto-generatable templates, spot-check only:**
|
|
70
|
+
|
|
71
|
+
- SHADCN-05 (`forwardRef`→ref-as-prop), SHADCN-06 (`data-slot`), SHADCN-09 (`tailwindcss-animate`→`tw-animate-css`), SHADCN-11 (`h-*/w-*`→`size-*`). Deterministic AST rewrites with no intended visual change. Weight **usage** (file count) heavily here, since these fan out across every component file and dominate the diff size.
|
|
72
|
+
|
|
73
|
+
**Opt-in — never blocking:**
|
|
74
|
+
|
|
75
|
+
- SHADCN-13 (Radix package consolidation) is dependency hygiene only. Gate it behind explicit user opt-in; never hold up a migration on it.
|
|
76
|
+
|
|
77
|
+
**Test signal:** rules touching files that already have component tests (often the primitives in SHADCN-05/06/11) score lower review-risk, because the suite catches regressions. Rules touching theme/CSS (SHADCN-01/02/04/15/16) are rarely unit-tested, score higher, and must lean on visual verification instead of the test gate.
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
# Tailwind CSS v3 → v4 — Migration Rule Library
|
|
2
|
+
|
|
3
|
+
This is a **proposal-only** rule library consumed by `agents/ds-migration-planner.md` and the pure `scripts/lib/migration/codemod-gen.cjs`. It encodes the concrete, mechanical changes between Tailwind CSS v3 and v4 so the planner can template codemods. GDD never auto-applies any rule below — every codemod is surfaced as a reviewable diff the USER inspects and runs.
|
|
4
|
+
|
|
5
|
+
A note the planner should carry into any plan it writes: v4 raised its browser baseline (it targets modern Safari / Chrome / Firefox and leans on native CSS cascade layers, `@property`, and `color-mix()`). A project that must support older browsers is **not** a clean migration target — the planner should surface that as a blocker rather than emitting codemods, since no class rename below can recover the dropped baseline.
|
|
6
|
+
|
|
7
|
+
## Detection
|
|
8
|
+
|
|
9
|
+
Detect Tailwind and the v3→v4 boundary from `package.json` **alone** — no file-content scanning is required for the gate. The planner reads `dependencies` + `devDependencies` and classifies:
|
|
10
|
+
|
|
11
|
+
- **Tailwind present** — a `tailwindcss` key appears under `dependencies` or `devDependencies`. If absent, the project is out of scope and no rules apply.
|
|
12
|
+
|
|
13
|
+
- **On v3** — the `tailwindcss` semver range resolves to `^3` / `~3` / `3.x`. Corroborating signals:
|
|
14
|
+
- a `tailwind.config.js` (or `.cjs` / `.mjs` / `.ts`) referenced by the build config, and
|
|
15
|
+
- a PostCSS pipeline that lists the bare `tailwindcss` plugin (in v3 the PostCSS plugin ships *inside* the core package).
|
|
16
|
+
|
|
17
|
+
- **On v4** — the `tailwindcss` range resolves to `^4` / `4.x`, **and/or** one of the dedicated build packages is present:
|
|
18
|
+
- `@tailwindcss/postcss` — the PostCSS plugin, which **moved out of core** into its own package in v4 (a high-confidence v4 marker), or
|
|
19
|
+
- `@tailwindcss/vite` — the first-party Vite plugin (used instead of PostCSS in Vite projects).
|
|
20
|
+
|
|
21
|
+
- **Migration candidate** — `tailwindcss@^3` with **no** `@tailwindcss/postcss` and **no** `@tailwindcss/vite`. A `tailwind.config.js` that survives the version bump is the signal that the JS-config → CSS-first migration (TW4-02) has not yet run.
|
|
22
|
+
|
|
23
|
+
- **Build-tool target** — a `vite` dependency steers the planner toward the `@tailwindcss/vite` install target; otherwise assume the `@tailwindcss/postcss` PostCSS target. Do **not** infer Tailwind from a lockfile alone — require the manifest entry so the gate stays deterministic.
|
|
24
|
+
|
|
25
|
+
Reading the semver range: treat a leading `^3` / `~3` / `3` / `3.x` as v3, and `^4` / `4` / `4.x` as v4. A wildcard (`*`) or git/`file:` specifier is **ambiguous** — the planner should fall back to the corroborating signals above (presence of `@tailwindcss/postcss`, a surviving `tailwind.config.js`) and, if still unresolved, mark detection as inconclusive rather than guessing. Detection is package.json-only by design; the per-class renames in the table are confirmed later against source by the codemod step, not by this gate.
|
|
26
|
+
|
|
27
|
+
## Migration rules
|
|
28
|
+
|
|
29
|
+
Rule IDs are stable. `Kind ∈ rename-class | rename-prop | remove-component | token-rename | new-default`.
|
|
30
|
+
|
|
31
|
+
| Rule ID | Kind | From → To | Note |
|
|
32
|
+
|---------|------|-----------|------|
|
|
33
|
+
| TW4-01 | rename-prop | `@tailwind base; @tailwind components; @tailwind utilities;` → `@import "tailwindcss";` | The three v3 directives collapse into a single CSS import. Fully mechanical; affects the one entry CSS file. |
|
|
34
|
+
| TW4-02 | token-rename | `tailwind.config.js` (JS `theme`) → `@theme { … }` (CSS-first) | Theme moves into CSS custom properties under `@theme`. The JS config is still loadable via an `@config "./tailwind.config.js";` bridge, but the native v4 target is CSS. Highest-effort rule in the set. |
|
|
35
|
+
| TW4-03 | token-rename | `theme(colors.red.500)` in CSS → `var(--color-red-500)` | `@theme` tokens are emitted as real CSS variables, so `theme()` lookups and arbitrary values resolve to `var(--…)` references instead. |
|
|
36
|
+
| TW4-04 | rename-class | `shadow-sm` → `shadow-xs` | Box-shadow scale shifted one step down. **High visual delta** — every `shadow-sm` renders visibly smaller after the bump. |
|
|
37
|
+
| TW4-05 | rename-class | `shadow` → `shadow-sm` | The bare `shadow` alias is removed; the old default maps to the new explicit `shadow-sm`. |
|
|
38
|
+
| TW4-06 | rename-class | `drop-shadow` → `drop-shadow-sm` | Same bare-alias removal as TW4-05, applied across the `drop-shadow-*` filter scale. |
|
|
39
|
+
| TW4-07 | rename-class | `blur-sm` → `blur-xs` | Blur scale shifted one step down, mirroring the shadow scale. |
|
|
40
|
+
| TW4-08 | rename-class | `blur` → `blur-sm` | Bare `blur` alias removed; the old default becomes the explicit `blur-sm`. |
|
|
41
|
+
| TW4-09 | rename-class | `rounded-sm` → `rounded-xs` | Border-radius scale shifted one step down. |
|
|
42
|
+
| TW4-10 | rename-class | `rounded` → `rounded-sm` | Bare `rounded` alias removed; the old default becomes the explicit `rounded-sm`. |
|
|
43
|
+
| TW4-11 | rename-class | `ring` → `ring-3` | The default ring **width changed 3px → 1px** in v4. To preserve the v3 look the bare `ring` must become `ring-3`. **High visual delta** if skipped. |
|
|
44
|
+
| TW4-12 | new-default | bare `ring` color `blue-500` → `currentColor` | v4's bare `ring` now derives from `currentColor`, not `blue-500`. Where the blue was intentional, add an explicit `ring-blue-500`. |
|
|
45
|
+
| TW4-13 | rename-class | `outline-none` → `outline-hidden` | `outline-none` now emits a real `outline: none` (it was a transparent-outline a11y shim in v3). The old shim behavior is renamed `outline-hidden`; audit focus-visible styling when applying. |
|
|
46
|
+
| TW4-14 | rename-class | `bg-opacity-50` / `text-opacity-*` / `border-opacity-*` → `bg-black/50` (slash) | The `*-opacity-*` utilities are removed in favor of color/opacity slash syntax (`bg-black/50`, `text-white/70`). The numeric opacity value carries over directly. |
|
|
47
|
+
| TW4-15 | rename-class | `flex-shrink-0` / `flex-grow` → `shrink-0` / `grow` | The `flex-` prefix on shrink/grow was already deprecated in v3 and is removed in v4. Pure prefix strip, no value change. |
|
|
48
|
+
| TW4-16 | new-default | default border / divide color `gray-200` → `currentColor` | v4 borders and dividers default to `currentColor`, not `gray-200`. To preserve the v3 look, set `@theme { --color-border: …; }` or add explicit `border-gray-200`. **High visual delta.** |
|
|
49
|
+
| TW4-17 | rename-prop | `@layer utilities { … }` (custom utility) → `@utility name { … }` | Registering custom utilities now uses the `@utility` API; plain `@layer utilities` blocks no longer register sortable utilities. `@layer base` for resets still works. |
|
|
50
|
+
| TW4-18 | rename-prop | `corePlugins` / `safelist` (JS config) → `@source inline(…)` + CSS theme | v4 has no `corePlugins` toggle and content detection is automatic; explicit safelisting moves to `@source inline(...)`. Flag for manual handling — not a 1:1 token swap. |
|
|
51
|
+
|
|
52
|
+
## Impact notes
|
|
53
|
+
|
|
54
|
+
**High visual delta — require manual review and a screenshot diff before accepting:**
|
|
55
|
+
|
|
56
|
+
- **Scale shifts (TW4-04 through TW4-10).** Shadow, blur, and radius scales all shifted one step down. A blanket find/replace is correct *only if* the project used the default scale. Any project that overrode these in `tailwind.config.js` must reconcile against TW4-02 first, or the rename compounds with a custom scale and yields wrong sizes. Treat these seven rules as one coordinated set, not independent edits.
|
|
57
|
+
- **Ring width + color (TW4-11, TW4-12).** The default ring went from 3px/blue to 1px/currentColor. Focus rings are an accessibility surface, so both the width (`ring-3`) and color (`ring-blue-500`) rewrites should be reviewed against focus-visible states rather than applied silently.
|
|
58
|
+
- **Default border / divide color (TW4-16) and outline (TW4-13).** The `currentColor` border default and the `outline-none` → `outline-hidden` rename change visible chrome and focus behavior across the whole app. These are the two rules most likely to look "broken" immediately post-migration; verify against a representative page set.
|
|
59
|
+
|
|
60
|
+
**Mechanical — low risk, still proposal-only:**
|
|
61
|
+
|
|
62
|
+
- **Import directive (TW4-01)** and **flex-prefix strip (TW4-15)** are deterministic textual swaps with no visual consequence — safe to batch, but still emitted as a reviewable diff.
|
|
63
|
+
- **Opacity slash syntax (TW4-14)** is mechanical per-utility, but the codemod must pair the correct color literal with the opacity value — confirm it preserves the original color when expanding `bg-opacity-50` into `bg-<color>/50`.
|
|
64
|
+
|
|
65
|
+
**Needs a human design decision — not auto-templatable:**
|
|
66
|
+
|
|
67
|
+
- **CSS-first theme (TW4-02), `theme()` rewrites (TW4-03), custom-utility migration (TW4-17), and config-feature removal (TW4-18)** depend on the project's actual theme shape and custom utilities. The planner should emit a scaffold (an `@theme` block skeleton plus an `@config` bridge note) and hand the remainder to the USER rather than attempting a full automatic translation. The `@config` directive is the recommended interim bridge so a project can adopt the v4 build packages *before* fully porting its JS theme.
|
|
68
|
+
|
|
69
|
+
**Application order and idempotency (for `codemod-gen.cjs`):**
|
|
70
|
+
|
|
71
|
+
- The shadow, blur, and radius scales each contain a **shift hazard**: renaming the bare alias up one step (e.g. TW4-05 `shadow` → `shadow-sm`) and shifting the named step down (TW4-04 `shadow-sm` → `shadow-xs`) target overlapping tokens. A naive two-pass run can double-apply — `shadow` becomes `shadow-sm` becomes `shadow-xs`. Templates must rename in a **single atomic pass** (match the original token, map to its final value) so each utility is rewritten exactly once. The same hazard applies to the `blur-*` (TW4-07/08) and `rounded-*` (TW4-09/10) pairs.
|
|
72
|
+
- Run the entry-CSS rules (**TW4-01**, then the **TW4-02 / TW4-03 / TW4-17** theme scaffold) before the class renames, so the build is parseable under v4 before utility classes change. Class renames (TW4-04 through TW4-16) operate on template/markup files and are independent of each other once the single-pass rule above is respected.
|
|
73
|
+
- Every generated codemod should be **idempotent** — re-running it on already-migrated source must be a no-op. The planner surfaces the full set as one reviewable changeset; the USER applies it, not GDD.
|
package/reference/registry.json
CHANGED
|
@@ -993,6 +993,41 @@
|
|
|
993
993
|
"type": "heuristic",
|
|
994
994
|
"phase": 38.5,
|
|
995
995
|
"description": "Phase 38.5 rollout-coordination contract: the <rollout_status> STATE block (unrolled/staging-only/canary-N%/prod-100%), stuck detection (default 14 days), linear deployed_pct weighting feeding design_arms via verify_outcome (a 10%-rolled variant counts 0.1), and the rollout_started/advanced/stuck/verify_outcome events. Classifier scripts/lib/rollout/rollout-status.cjs; agent agents/rollout-coordinator.md; skill /gdd:rollout-status. Read-only — GDD never drives the rollout."
|
|
996
|
+
},
|
|
997
|
+
{
|
|
998
|
+
"name": "shadcn-v2",
|
|
999
|
+
"path": "reference/migrations/shadcn-v2.md",
|
|
1000
|
+
"type": "heuristic",
|
|
1001
|
+
"phase": 39.1,
|
|
1002
|
+
"description": "Phase 39.1 migration rule library — shadcn/ui v1→v2 (Tailwind v4 alignment, OKLCH CSS vars, sonner, Radix React-19 refs). Migration-rules table (id/kind/from→to) + Detection + Impact; consumed by agents/ds-migration-planner.md + scripts/lib/migration/codemod-gen.cjs (proposal-only)."
|
|
1003
|
+
},
|
|
1004
|
+
{
|
|
1005
|
+
"name": "tailwind-v4",
|
|
1006
|
+
"path": "reference/migrations/tailwind-v4.md",
|
|
1007
|
+
"type": "heuristic",
|
|
1008
|
+
"phase": 39.1,
|
|
1009
|
+
"description": "Phase 39.1 migration rule library — Tailwind CSS v3→v4 (@import directive, CSS-first @theme, renamed shadow/blur/radius/ring scale, opacity slash syntax, default border color). Rules + Detection + Impact; codemod-gen-consumable."
|
|
1010
|
+
},
|
|
1011
|
+
{
|
|
1012
|
+
"name": "mui-v6",
|
|
1013
|
+
"path": "reference/migrations/mui-v6.md",
|
|
1014
|
+
"type": "heuristic",
|
|
1015
|
+
"phase": 39.1,
|
|
1016
|
+
"description": "Phase 39.1 migration rule library — MUI (Material UI) v5→v6 (Grid2-as-default, cssVariables theme, extendTheme/CssVarsProvider de-prefixing, theme.vars). Rules + Detection + Impact; codemod-gen-consumable."
|
|
1017
|
+
},
|
|
1018
|
+
{
|
|
1019
|
+
"name": "material-3-to-4",
|
|
1020
|
+
"path": "reference/migrations/material-3-to-4.md",
|
|
1021
|
+
"type": "heuristic",
|
|
1022
|
+
"phase": 39.1,
|
|
1023
|
+
"description": "Phase 39.1 migration rule library — Material Design token migration (M3→next), grounded in the real M2→M3 token-system patterns (md.sys.color/typescale roles, @material/web mwc-→md-) — no fabricated M4 spec. Rules + Detection + Impact; codemod-gen-consumable."
|
|
1024
|
+
},
|
|
1025
|
+
{
|
|
1026
|
+
"name": "cost-governance",
|
|
1027
|
+
"path": "reference/cost-governance.md",
|
|
1028
|
+
"type": "heuristic",
|
|
1029
|
+
"phase": 39.2,
|
|
1030
|
+
"description": "Phase 39.2 cost-governance contract: the per-cycle forecast model (best/typical/worst from mean ± k·σ, cyclesToCap) via scripts/lib/budget/cost-forecast.cjs; the project_cap hard-halt (disabled by default, graceful PreToolUse:Agent block, warn 50/80 + halt 100) via scripts/lib/budget/project-cap.cjs + hooks/budget-enforcer.ts; the ROI dashboard (shipped = surviving >=14d, cost-per-shipped-commit) via scripts/lib/budget/roi.cjs; and the budget_forecast/project_cap_warning/project_cap_halt events. Agent agents/cost-forecaster.md; skills /gdd:budget + /gdd:roi. Read/report-only — the hook only blocks, never spends."
|
|
996
1031
|
}
|
|
997
1032
|
]
|
|
998
1033
|
}
|
|
@@ -37,6 +37,16 @@
|
|
|
37
37
|
"type": "string",
|
|
38
38
|
"enum": ["enforce", "warn", "log"],
|
|
39
39
|
"description": "D-11 enforcement policy. enforce = block + auto-downgrade; warn = print warnings but allow spawn; log = advisory-only telemetry without gating."
|
|
40
|
+
},
|
|
41
|
+
"project_cap_usd": {
|
|
42
|
+
"type": "number",
|
|
43
|
+
"minimum": 0,
|
|
44
|
+
"description": "Phase 39.2 D-04 — project-level hard cap (USD) across the whole project's costs.jsonl. 0 or absent = DISABLED (no project-level enforcement; zero behavior change for existing users). When > 0, hooks/budget-enforcer.ts warns at 50% + 80% of this cap and (under project_cap_enforcement_mode=enforce) hard-halts the next PreToolUse:Agent spawn at 100%. Distinct from per_task_cap_usd / per_phase_cap_usd."
|
|
45
|
+
},
|
|
46
|
+
"project_cap_enforcement_mode": {
|
|
47
|
+
"type": "string",
|
|
48
|
+
"enum": ["enforce", "warn", "log"],
|
|
49
|
+
"description": "Phase 39.2 D-04 — enforcement policy for project_cap_usd specifically. enforce = hard-halt at 100%; warn = print at 100% but allow; log = advisory telemetry only. Falls back to enforcement_mode when absent."
|
|
40
50
|
}
|
|
41
51
|
}
|
|
42
52
|
}
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
"type": {
|
|
11
11
|
"type": "string",
|
|
12
12
|
"minLength": 1,
|
|
13
|
-
"description": "Free-form event type identifier. Pre-registered seeds: state.mutation, state.transition, stage.entered, stage.exited, hook.fired, error, capability_gap, kfm-candidate, router_pick, verify_outcome, rollout_started, rollout_advanced, rollout_stuck."
|
|
13
|
+
"description": "Free-form event type identifier. Pre-registered seeds: state.mutation, state.transition, stage.entered, stage.exited, hook.fired, error, capability_gap, kfm-candidate, router_pick, verify_outcome, rollout_started, rollout_advanced, rollout_stuck, budget_forecast, project_cap_warning, project_cap_halt."
|
|
14
14
|
},
|
|
15
15
|
"timestamp": {
|
|
16
16
|
"type": "string",
|
|
@@ -58,6 +58,14 @@ export interface DesignBudgetJson {
|
|
|
58
58
|
* D-11 enforcement policy. enforce = block + auto-downgrade; warn = print warnings but allow spawn; log = advisory-only telemetry without gating.
|
|
59
59
|
*/
|
|
60
60
|
enforcement_mode?: 'enforce' | 'warn' | 'log';
|
|
61
|
+
/**
|
|
62
|
+
* Phase 39.2 D-04 — project-level hard cap (USD) across the whole project's costs.jsonl. 0 or absent = DISABLED (no project-level enforcement; zero behavior change for existing users). When > 0, hooks/budget-enforcer.ts warns at 50% + 80% of this cap and (under project_cap_enforcement_mode=enforce) hard-halts the next PreToolUse:Agent spawn at 100%. Distinct from per_task_cap_usd / per_phase_cap_usd.
|
|
63
|
+
*/
|
|
64
|
+
project_cap_usd?: number;
|
|
65
|
+
/**
|
|
66
|
+
* Phase 39.2 D-04 — enforcement policy for project_cap_usd specifically. enforce = hard-halt at 100%; warn = print at 100% but allow; log = advisory telemetry only. Falls back to enforcement_mode when absent.
|
|
67
|
+
*/
|
|
68
|
+
project_cap_enforcement_mode?: 'enforce' | 'warn' | 'log';
|
|
61
69
|
[k: string]: unknown;
|
|
62
70
|
}
|
|
63
71
|
|
|
@@ -106,7 +114,7 @@ export type Event = {
|
|
|
106
114
|
[k: string]: unknown;
|
|
107
115
|
} & {
|
|
108
116
|
/**
|
|
109
|
-
* Free-form event type identifier. Pre-registered seeds: state.mutation, state.transition, stage.entered, stage.exited, hook.fired, error, capability_gap.
|
|
117
|
+
* Free-form event type identifier. Pre-registered seeds: state.mutation, state.transition, stage.entered, stage.exited, hook.fired, error, capability_gap, kfm-candidate, router_pick, verify_outcome, rollout_started, rollout_advanced, rollout_stuck, budget_forecast, project_cap_warning, project_cap_halt.
|
|
110
118
|
*/
|
|
111
119
|
type: string;
|
|
112
120
|
/**
|
|
@@ -581,6 +589,62 @@ export interface ClaudePluginJson {
|
|
|
581
589
|
|
|
582
590
|
export type PluginSchema = ClaudePluginJson;
|
|
583
591
|
|
|
592
|
+
// ---- pressure-scenario.schema.json ----
|
|
593
|
+
/**
|
|
594
|
+
* Contract for a Phase-33 skill-behavior pressure-scenario manifest. The runner (scripts/lib/skill-behavior/runner.cjs) loads manifests conforming to this schema, spawns a subagent against `setup_prompt` under the named `pressures`, and validates the response against the `expected_compliance` / `expected_violations` regex sources (compiled with new RegExp(source)). The 5-value `pressures` enum and the required-field set come verbatim from ROADMAP Phase-33 SC#2.
|
|
595
|
+
*/
|
|
596
|
+
export interface PressureScenarioManifest {
|
|
597
|
+
/**
|
|
598
|
+
* Unique scenario identifier, e.g. "brief-time-pressure".
|
|
599
|
+
*/
|
|
600
|
+
name: string;
|
|
601
|
+
/**
|
|
602
|
+
* The skill under test, e.g. "brief", "explore", "plan", "using-gdd".
|
|
603
|
+
*/
|
|
604
|
+
target_skill: string;
|
|
605
|
+
/**
|
|
606
|
+
* One or more pressure vectors applied in the setup_prompt.
|
|
607
|
+
*
|
|
608
|
+
* @minItems 1
|
|
609
|
+
*/
|
|
610
|
+
pressures: [
|
|
611
|
+
'time' | 'sunk-cost' | 'authority' | 'exhaustion' | 'scope-minimization',
|
|
612
|
+
...('time' | 'sunk-cost' | 'authority' | 'exhaustion' | 'scope-minimization')[],
|
|
613
|
+
];
|
|
614
|
+
/**
|
|
615
|
+
* The prompt handed to the subagent — embeds the pressure(s) and asks it to act.
|
|
616
|
+
*/
|
|
617
|
+
setup_prompt: string;
|
|
618
|
+
/**
|
|
619
|
+
* Regex SOURCE strings the response MUST match to count as compliant (the runner compiles each with new RegExp(source)).
|
|
620
|
+
*
|
|
621
|
+
* @minItems 1
|
|
622
|
+
*/
|
|
623
|
+
expected_compliance: [string, ...string[]];
|
|
624
|
+
/**
|
|
625
|
+
* Regex SOURCE strings that, if matched, count as a violation (the runner compiles each with new RegExp(source)). May be empty.
|
|
626
|
+
*/
|
|
627
|
+
expected_violations: string[];
|
|
628
|
+
/**
|
|
629
|
+
* Optional free-text scenario note (33-03 baselines reference it).
|
|
630
|
+
*/
|
|
631
|
+
description?: string;
|
|
632
|
+
/**
|
|
633
|
+
* Optional A/B variant label, e.g. "trigger-only" | "what-clause" (33-04 description-format A/B).
|
|
634
|
+
*/
|
|
635
|
+
variant?: string;
|
|
636
|
+
/**
|
|
637
|
+
* Optional array of A/B variant descriptors for a single-manifest A/B pair (33-04). Each item is an object, e.g. { label, description }.
|
|
638
|
+
*/
|
|
639
|
+
variants?: {}[];
|
|
640
|
+
/**
|
|
641
|
+
* Optional body-only probe prompt the A/B scenario asks (33-04 description-format A/B).
|
|
642
|
+
*/
|
|
643
|
+
body_probe?: string;
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
export type PressureScenarioSchema = PressureScenarioManifest;
|
|
647
|
+
|
|
584
648
|
// ---- protected-paths.schema.json ----
|
|
585
649
|
/**
|
|
586
650
|
* Glob list describing paths the plugin refuses to Edit/Write or mutate via destructive Bash. User additions MERGE with this default list; users cannot reduce the default set.
|
|
@@ -622,6 +686,35 @@ export interface RateLimits {
|
|
|
622
686
|
|
|
623
687
|
export type RateLimitsSchema = RateLimits;
|
|
624
688
|
|
|
689
|
+
// ---- recipe.schema.json ----
|
|
690
|
+
/**
|
|
691
|
+
* Shape of a declarative recipe loaded from recipes/<name>.json by scripts/lib/recipe-loader.cjs (Plan 31-5-03, RECIPE-01 / SC#14). The recipes/ directory ships EMPTY of recipes and is populated downstream by Phase 32 (skill-trigger recipes), Phase 33.6 (per-provider), Phase 26 (per-runtime/per-model), and Phase 23.5 (bandit-arm shape). This is a minimal, forward-compatible envelope: a recipe MUST carry name/version/steps; additionalProperties:true lets the populating phases extend the envelope without breaking the loader contract. Modelled on Storybloq's src/autonomous/recipes/ loader.ts pattern.
|
|
692
|
+
*/
|
|
693
|
+
export interface Recipe {
|
|
694
|
+
/**
|
|
695
|
+
* The recipe identifier. Matches the filename stem (recipes/<name>.json).
|
|
696
|
+
*/
|
|
697
|
+
name: string;
|
|
698
|
+
/**
|
|
699
|
+
* Recipe/schema version string for forward-compatibility. Lets the loader and downstream phases reason about envelope evolution.
|
|
700
|
+
*/
|
|
701
|
+
version: string;
|
|
702
|
+
/**
|
|
703
|
+
* The ordered recipe body. Item shape is kept permissive for now — each step is an object carrying at least a `kind` OR an `id` string. Downstream phases (32/33.6/26/23.5) tighten the step contract per their domain.
|
|
704
|
+
*/
|
|
705
|
+
steps: (
|
|
706
|
+
| {
|
|
707
|
+
kind: string;
|
|
708
|
+
}
|
|
709
|
+
| {
|
|
710
|
+
id: string;
|
|
711
|
+
}
|
|
712
|
+
)[];
|
|
713
|
+
[k: string]: unknown;
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
export type RecipeSchema = Recipe;
|
|
717
|
+
|
|
625
718
|
// ---- runtime-models.schema.json ----
|
|
626
719
|
/**
|
|
627
720
|
* Parsed shape of reference/runtime-models.md — the per-runtime tier→model adapter source-of-truth shipped in Phase 26 (D-01..D-03). Consumed by scripts/lib/install/parse-runtime-models.cjs at install time and scripts/lib/tier-resolver.cjs at runtime. Strict enums catch typos at install time, not at runtime. Schema versioned via $schema_version for forward-compat (D-03).
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
// Phase 39.2 — cost-forecast.cjs — PURE, dep-free per-cycle cost forecasting core.
|
|
3
|
+
//
|
|
4
|
+
// The /gdd:budget skill and agents/cost-forecaster.md read .design/telemetry/costs.jsonl, group the
|
|
5
|
+
// est_cost_usd by `cycle`, and hand the resulting per-cycle USD totals here. This module does ONLY
|
|
6
|
+
// the projection math — it never touches the filesystem, the clock, or randomness, so it is trivially
|
|
7
|
+
// unit-testable (the build-html.cjs / codemod-gen.cjs purity precedent).
|
|
8
|
+
//
|
|
9
|
+
// Scenario derivation (D-05): from the variance of the historical per-cycle rates,
|
|
10
|
+
// typical = mean
|
|
11
|
+
// worst = mean + k·stddev
|
|
12
|
+
// best = max(0, mean − k·stddev)
|
|
13
|
+
// with k = 1 by default. Projection over the next N cycles is linear on the chosen rate.
|
|
14
|
+
//
|
|
15
|
+
// No `require` — pure. Deterministic.
|
|
16
|
+
|
|
17
|
+
/** Coerce to a finite, non-negative number or throw. */
|
|
18
|
+
function num(x, label) {
|
|
19
|
+
const n = Number(x);
|
|
20
|
+
if (!Number.isFinite(n)) throw new Error(`cost-forecast: ${label} must be a finite number (got ${x})`);
|
|
21
|
+
return n;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** Population mean of an array of numbers (0 for empty). */
|
|
25
|
+
function mean(xs) {
|
|
26
|
+
if (!xs.length) return 0;
|
|
27
|
+
let s = 0;
|
|
28
|
+
for (const x of xs) s += x;
|
|
29
|
+
return s / xs.length;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** Population standard deviation (0 for length < 2). */
|
|
33
|
+
function stddev(xs) {
|
|
34
|
+
if (xs.length < 2) return 0;
|
|
35
|
+
const m = mean(xs);
|
|
36
|
+
let acc = 0;
|
|
37
|
+
for (const x of xs) acc += (x - m) * (x - m);
|
|
38
|
+
return Math.sqrt(acc / xs.length);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Normalize the cycle-cost input into a clean array of non-negative per-cycle USD totals.
|
|
43
|
+
* Accepts either an array of numbers, or an array of { costUsd } / { est_cost_usd } objects.
|
|
44
|
+
*/
|
|
45
|
+
function perCycleRates(cycleCosts) {
|
|
46
|
+
if (!Array.isArray(cycleCosts)) throw new Error('cost-forecast: cycleCosts must be an array');
|
|
47
|
+
return cycleCosts.map((c, i) => {
|
|
48
|
+
const v = typeof c === 'object' && c !== null
|
|
49
|
+
? (c.costUsd !== undefined ? c.costUsd : c.est_cost_usd)
|
|
50
|
+
: c;
|
|
51
|
+
const n = num(v, `cycleCosts[${i}]`);
|
|
52
|
+
return n < 0 ? 0 : n;
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Project the next `nCycles` of spend.
|
|
58
|
+
* @returns {{scenario, k, observedCycles, perCycle, projectedTotal, low, high}}
|
|
59
|
+
* perCycle — the per-cycle rate used for this scenario
|
|
60
|
+
* projectedTotal — perCycle * nCycles
|
|
61
|
+
* low/high — the best/worst per-cycle band (always returned for context)
|
|
62
|
+
*/
|
|
63
|
+
function forecast(cycleCosts, opts) {
|
|
64
|
+
const o = opts || {};
|
|
65
|
+
const nCycles = o.nCycles === undefined ? 5 : Math.max(0, Math.trunc(num(o.nCycles, 'nCycles')));
|
|
66
|
+
const scenario = o.scenario === undefined ? 'typical' : String(o.scenario);
|
|
67
|
+
const k = o.k === undefined ? 1 : num(o.k, 'k');
|
|
68
|
+
if (!['best', 'typical', 'worst'].includes(scenario)) {
|
|
69
|
+
throw new Error(`cost-forecast: scenario must be best|typical|worst (got ${scenario})`);
|
|
70
|
+
}
|
|
71
|
+
const rates = perCycleRates(cycleCosts);
|
|
72
|
+
const m = mean(rates);
|
|
73
|
+
const sd = stddev(rates);
|
|
74
|
+
const low = Math.max(0, m - k * sd);
|
|
75
|
+
const high = m + k * sd;
|
|
76
|
+
const perCycle = scenario === 'best' ? low : scenario === 'worst' ? high : m;
|
|
77
|
+
return {
|
|
78
|
+
scenario,
|
|
79
|
+
k,
|
|
80
|
+
observedCycles: rates.length,
|
|
81
|
+
perCycle,
|
|
82
|
+
projectedTotal: perCycle * nCycles,
|
|
83
|
+
low,
|
|
84
|
+
high,
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Integer count of full cycles until `currentSpend` reaches `cap` at `perCycleRate`.
|
|
90
|
+
* - rate <= 0 → Infinity (never reaches cap)
|
|
91
|
+
* - currentSpend >= cap → 0 (already at/over)
|
|
92
|
+
* Throws on non-finite inputs.
|
|
93
|
+
*/
|
|
94
|
+
function cyclesToCap(currentSpend, cap, perCycleRate) {
|
|
95
|
+
const s = num(currentSpend, 'currentSpend');
|
|
96
|
+
const c = num(cap, 'cap');
|
|
97
|
+
const r = num(perCycleRate, 'perCycleRate');
|
|
98
|
+
if (s >= c) return 0;
|
|
99
|
+
if (r <= 0) return Infinity;
|
|
100
|
+
return Math.ceil((c - s) / r);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
module.exports = { perCycleRates, mean, stddev, forecast, cyclesToCap };
|