diff --git a/.github/workflows/auto-pr.yml b/.github/workflows/auto-pr.yml index 27405df..d755182 100644 --- a/.github/workflows/auto-pr.yml +++ b/.github/workflows/auto-pr.yml @@ -4,6 +4,12 @@ name: Auto PR (develop → main) # is a one-click merge. Copied from max-network/worker-utils' auto-pr.yml (same flow in # Max-Health-Inc/mcp-http and BabelFHIR-TS) rather than reinvented. # +# STAYS A COPY, unlike the other max-network repos, which now call the shared definition in +# max-network/.github. This repository is PUBLIC and that one is private, and a public repo +# cannot call a private repo's reusable workflow — the attempt fails at startup with no job and +# no log. hono-cms-core is in the same position for the same reason. Making the shared repo +# public would let both join; until that call is made, this copy is deliberate. +# # feature branch ──PR──> develop ──(this)──> standing PR ──merge──> main ──> publish # # Before this, `main` WAS the working branch: every PR targeted it and merging published to npm diff --git a/.github/workflows/check.yml b/.github/workflows/check.yml new file mode 100644 index 0000000..fec9dbd --- /dev/null +++ b/.github/workflows/check.yml @@ -0,0 +1,20 @@ +name: Check + +# The quality gate: `npm run check` (tsc --noEmit, build, node --test). Runs on both integration +# branches and on every PR, so a change is verified where it lands (develop) and where it ships +# (main). Named for what it runs, matching the `check` script and worker-utils' checks.yml. +on: + push: + branches: [main, develop] + pull_request: + +jobs: + check: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: 22 + - run: npm install + - run: npm run check diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml deleted file mode 100644 index 17eb810..0000000 --- a/.github/workflows/ci.yml +++ /dev/null @@ -1,19 +0,0 @@ -name: CI - -# The quality gate. Runs on both integration branches and on every PR, so a change is checked -# where it lands (develop) as well as where it ships (main). -on: - push: - branches: [main, develop] - pull_request: - -jobs: - check: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - uses: actions/setup-node@v4 - with: - node-version: 22 - - run: npm install - - run: npm run check diff --git a/CHANGELOG.md b/CHANGELOG.md index 8a76a76..22b45ce 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,8 +5,63 @@ Format: [Keep a Changelog](https://keepachangelog.com/en/1.1.0/); versioning: [S ## [Unreleased] +## [0.7.0] — 2026-08-30 + +### Added + +- **The radius ladder is complete: `radius-xl`, `radius-2xl`, `radius-3xl`, `radius-4xl`.** + The contract stopped at `radius-lg`, and a step a brand does not define falls through to + Tailwind's own default — so `maxhealth`, which is flat by design, still rendered + `rounded-xl` at 0.75rem and `rounded-3xl` at 1.5rem. Every consumer re-zeroed the tail by + hand in its own `@theme inline` block (proxy-smart-admin-ui, patient-portal, consent-app), + which is exactly the drift a shared contract exists to prevent. `maxhealth` sets all four + to `0rem`; `dashboard` continues its ladder at 1 / 1.5 / 2 / 2.5rem. + + Contract size is now 82 tokens (52 colours + 30 scalars). + +### Added + +- **The rhythm half of the contract: a type scale and a spacing scale** ([#18]). Documented here + rather than under 0.6.1 because that release went out with no notes; the code itself shipped in + 0.6.1, as a patch, which is the mislabelling the release-gate and workflow fixes below address. + + The contract carried shape, elevation and typeface but nothing for size or density, so every + consumer invented its own and pages stopped sharing a rhythm. Adds 16 scalars — `--text-xs` … + `--text-2xl`, `--space-1` … `--space-8`, and `--measure` — taking the contract from 62 to 78 names. + + Values are plain `rem` on purpose: `toPrefabTheme` and the React Native path read scalars as + values, so a `clamp()` with viewport units would be unreadable there. A kit that wants fluid + headings clamps these itself. Both shipped brands start from one shared `RHYTHM` and override + any step to change density, exactly as they override `--radius` to change shape. + + Additive for consumers: only a brand typed `satisfies ContractBrand` must supply the new names. + +### Fixed + +- **The release gate no longer fails open** ([#19]). `changelog-release.mjs` refused to release an + undocumented change in principle only. Its regex let `\s*\n` consume the newline the lookahead + needed, so an EMPTY `[Unreleased]` captured the *next* release's section instead — non-empty, so + the guard passed, and reinserting it hid the damage. That is how 0.6.1 shipped with no notes at + all. Sections are now found by index, and the behaviour is pinned by `test/changelog.test.mjs`. + +### Changed + +- **`develop` is the integration branch; releasing is bumping the version in your PR** ([#19]). + `main` used to be the working branch, so every PR targeted it and merging published on the spot. + A push to `develop` now keeps a standing promote PR open (`auto-pr.yml`, the same file + `worker-utils` and `mcp-http` use). `release.yml` no longer auto-increments the patch — that is + why 0.6.1 went out as a patch when it added 16 contract names. `package.json` is the release, and + a merge whose version is already published is a green no-op. +- `ci.yml` → `check.yml`, named for the script it runs. + +[#18]: https://github.com/max-network/brandc/issues/18 +[#19]: https://github.com/max-network/brandc/issues/19 + ## [0.6.1] — 2026-08-08 +Released by a workflow that stamped the version without notes; see the release-gate fix above. +The contract additions listed under Unreleased are the content that actually shipped here. + ## [0.6.0] — 2026-07-31 ### Fixed diff --git a/README.md b/README.md index 882f23c..602b8c9 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,7 @@ Ships an example brand you can use as-is or override. - **The contract** — the *names* of the variables (`--primary`, `--card`, `--radius`, `--success`, `--font-sans`, …). Stable across kits and stacks. Declared — not derived from any brand — in - [`src/contract.ts`](src/contract.ts): 78 names, as `CONTRACT` / `CONTRACT_COLORS` / + [`src/contract.ts`](src/contract.ts): 82 names, as `CONTRACT` / `CONTRACT_COLORS` / `CONTRACT_SCALARS` and the `TokenName` union. No token is named after a brand, so any brand can satisfy it. The scalars cover shape (`--radius*`), elevation (`--shadow*`), typography (`--font-*`) and rhythm: a type scale (`--text-xs` … `--text-2xl`), a spacing ramp diff --git a/package-lock.json b/package-lock.json index e66c147..a889dcb 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,407 +1,46 @@ { "name": "brandc", - "version": "0.6.1", + "version": "0.6.2", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "brandc", - "version": "0.6.1", + "version": "0.6.2", "license": "MIT", "devDependencies": { - "@types/node": "^24.0.0", - "typescript": "^7.0.2" + "@types/node": "^22.20.1", + "typescript": "^6.0.3" } }, "node_modules/@types/node": { - "version": "24.13.3", - "resolved": "https://registry.npmjs.org/@types/node/-/node-24.13.3.tgz", - "integrity": "sha512-Dh8vAsV36ig5wa9OX4pXvMc9D3Veibfw2wix0CUwYODLD8nkj9UsLjASr49nPg+2eKzxhBV+v7L8pXvT4e639Q==", + "version": "22.20.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.20.1.tgz", + "integrity": "sha512-EANqOCF9QFyra+4pfxUcX9STKJpCLjMbObVzljIJomAWSnuSIEAvyzEU53GaajbXJEgdh0iEcPL+DGvpUd4k1Q==", "dev": true, "license": "MIT", "dependencies": { - "undici-types": "~7.18.0" - } - }, - "node_modules/@typescript/typescript-aix-ppc64": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/@typescript/typescript-aix-ppc64/-/typescript-aix-ppc64-7.0.2.tgz", - "integrity": "sha512-MTKKkWB7p/0E9xi1d1tHtZ5PiLkGEMIq88pK2CubZjOsLtYTLqhgIgi6zepFa+9GHZ6h05NMCkQxGKiPXMxXtQ==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "Apache-2.0", - "optional": true, - "os": [ - "aix" - ], - "engines": { - "node": ">=16.20.0" - } - }, - "node_modules/@typescript/typescript-darwin-arm64": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/@typescript/typescript-darwin-arm64/-/typescript-darwin-arm64-7.0.2.tgz", - "integrity": "sha512-gowzar9MwS/aRWp6f3a4KUqzRjAZjOsmGNCM6LcTgXum+dBfgsBVMN+AgvOCCbguXyick6LJhpBszxMebJ8syA==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "Apache-2.0", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=16.20.0" - } - }, - "node_modules/@typescript/typescript-darwin-x64": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/@typescript/typescript-darwin-x64/-/typescript-darwin-x64-7.0.2.tgz", - "integrity": "sha512-SZ9xZInqApNlNGc9s0W1VSsktYSOe9cFqNOIqmN1Gs8SmkjKZYFt017G4VwPxASInODuAdbTW7sXiFUf893RgA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "Apache-2.0", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=16.20.0" - } - }, - "node_modules/@typescript/typescript-freebsd-arm64": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/@typescript/typescript-freebsd-arm64/-/typescript-freebsd-arm64-7.0.2.tgz", - "integrity": "sha512-W5NH4y/J0plIIS5b2xvTEkU7JFxyqdMAOgf+Ilhl0vHQXKO5dZoxd+C/jEtq56c4F3wk71RB4BMRQ2XdI+bwYQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "Apache-2.0", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=16.20.0" - } - }, - "node_modules/@typescript/typescript-freebsd-x64": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/@typescript/typescript-freebsd-x64/-/typescript-freebsd-x64-7.0.2.tgz", - "integrity": "sha512-UMGDx5sTpzNw3WiPebH7l90IWfJggEd+egHt/q6p7/Cm3zqoV7VxkGXt+3DxPIw8CcmvAB0j3sVVfbhX+M4Tpw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "Apache-2.0", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=16.20.0" - } - }, - "node_modules/@typescript/typescript-linux-arm": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-arm/-/typescript-linux-arm-7.0.2.tgz", - "integrity": "sha512-gffT3xPz9sR7j/YJExkyPntrI0P2EP9XbOyWzth2/Gs0RstK+90RBcO0ncXoXy/beYll1SXw846Nf2zdnEz0QQ==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=16.20.0" - } - }, - "node_modules/@typescript/typescript-linux-arm64": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-arm64/-/typescript-linux-arm64-7.0.2.tgz", - "integrity": "sha512-Qh4eU4/y3yDjnfjjyPYihMj5/ODIlmt+Bzu17OI+fiSRDW57QmU5SiN63exPRNJPKUzcc1INa1NXdrJ+MqHjUQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=16.20.0" - } - }, - "node_modules/@typescript/typescript-linux-loong64": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-loong64/-/typescript-linux-loong64-7.0.2.tgz", - "integrity": "sha512-uEHck9i8hoAzXPiYRib1O7miOnz23SxIeVl6F4LXox+qov1K35jHcEW6VHKvZI+pyvl7fZEP4MCU5LYvIq1GuQ==", - "cpu": [ - "loong64" - ], - "dev": true, - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=16.20.0" - } - }, - "node_modules/@typescript/typescript-linux-mips64el": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-mips64el/-/typescript-linux-mips64el-7.0.2.tgz", - "integrity": "sha512-R4KvAMnE43W5Qeqb0Ly56O3mWMWIAgsMyz36DCaycd5nbg/9kzm0liw3JocfRqyJY0KPmzFjbswozXyW0DnIYA==", - "cpu": [ - "mips64el" - ], - "dev": true, - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=16.20.0" - } - }, - "node_modules/@typescript/typescript-linux-ppc64": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-ppc64/-/typescript-linux-ppc64-7.0.2.tgz", - "integrity": "sha512-DORx5b3sd/4S7eayxm4FQv+A7CrkUIGRaHiwI8oiHTAI1fAPWhF4J0vAlkC8biAlHSVVwxMQ3tjZ2/DVbnQiiA==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=16.20.0" - } - }, - "node_modules/@typescript/typescript-linux-riscv64": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-riscv64/-/typescript-linux-riscv64-7.0.2.tgz", - "integrity": "sha512-wf0jqEDOjrPRnKwYRyyJDRo11KMbvMFrU+q4zqKyChODBzvlkbhNQfKvLxQCcwTpdDaXSHZTVuh0JoCrKCUMHQ==", - "cpu": [ - "riscv64" - ], - "dev": true, - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=16.20.0" - } - }, - "node_modules/@typescript/typescript-linux-s390x": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-s390x/-/typescript-linux-s390x-7.0.2.tgz", - "integrity": "sha512-IkwJc3L7yhytWd/ewjyxNDfOmswCm9GWMJT/ue/dU4aZNbwZeYAetq42VyLmsmSjvoX7z74X6ZaYCtzAr0EuGw==", - "cpu": [ - "s390x" - ], - "dev": true, - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=16.20.0" - } - }, - "node_modules/@typescript/typescript-linux-x64": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-x64/-/typescript-linux-x64-7.0.2.tgz", - "integrity": "sha512-EYdf2cNg7rgCWJnxCdJ+F3V39O8ihb37eHAu1LK8oAFizgTQbPOK7zHHXbPt8rX24COqODXeI3sIf0fCXG7H/A==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=16.20.0" - } - }, - "node_modules/@typescript/typescript-netbsd-arm64": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/@typescript/typescript-netbsd-arm64/-/typescript-netbsd-arm64-7.0.2.tgz", - "integrity": "sha512-+polYF4MF04aPpO5FTkHran9yUQDSXqy5GiSDKpsll5jy3l3+g9QLhpf39T+ePtefhXLOGrLl0QIjkQP6VnelA==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "Apache-2.0", - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=16.20.0" - } - }, - "node_modules/@typescript/typescript-netbsd-x64": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/@typescript/typescript-netbsd-x64/-/typescript-netbsd-x64-7.0.2.tgz", - "integrity": "sha512-8YIT0EHM/3dq10ZOVF/A7pc/YSMtbcecct4rWtexrnSCHOPcpC2KTLXfTCR6vDpnSiY12heNb1GiN/wu+T/FyA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "Apache-2.0", - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=16.20.0" - } - }, - "node_modules/@typescript/typescript-openbsd-arm64": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/@typescript/typescript-openbsd-arm64/-/typescript-openbsd-arm64-7.0.2.tgz", - "integrity": "sha512-APT8+ClYnuYm1u9+kgGXoMj2VzWzcymwh2gNSQVySHfkRDGOTVkoWLjCmOQSaO+PoqQ57B0flRp9SA+7GnnkzQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "Apache-2.0", - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=16.20.0" - } - }, - "node_modules/@typescript/typescript-openbsd-x64": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/@typescript/typescript-openbsd-x64/-/typescript-openbsd-x64-7.0.2.tgz", - "integrity": "sha512-yX7s+Q0Dln0Dt9tEzZsAjXXR/+ytBM7AlglaqyeMPxQszJ1JhlJdZ6jLA+IzldHtflX81em7lDao1xXu+aRRkg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "Apache-2.0", - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=16.20.0" - } - }, - "node_modules/@typescript/typescript-sunos-x64": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/@typescript/typescript-sunos-x64/-/typescript-sunos-x64-7.0.2.tgz", - "integrity": "sha512-dLJDGaLZ1D4HPQn62u1n8mBDkJREwMsAkCdkwd4Ieqw+x3TUyTsqY0YiBCtE6H6OzzgGk3iuZ3vFWRS+E8/d1g==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "Apache-2.0", - "optional": true, - "os": [ - "sunos" - ], - "engines": { - "node": ">=16.20.0" - } - }, - "node_modules/@typescript/typescript-win32-arm64": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/@typescript/typescript-win32-arm64/-/typescript-win32-arm64-7.0.2.tgz", - "integrity": "sha512-Gyl1Vy6OsWesLzmq+EP0Fb7b4Nid5232AvcA2SFcdYreldpNtYFFofPjnt62y9hQy7VTaZp65ICJjuAQRaVcIQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "Apache-2.0", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=16.20.0" - } - }, - "node_modules/@typescript/typescript-win32-x64": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/@typescript/typescript-win32-x64/-/typescript-win32-x64-7.0.2.tgz", - "integrity": "sha512-0BQ3HkAHHlKLSp1qRvf3SUhGpGsDuhB/jgFw75guyqbxJqEaS0Cw/VFO8i2nHglJUzQCRtMMR/IBAKE3ETMC4g==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "Apache-2.0", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=16.20.0" + "undici-types": "~6.21.0" } }, "node_modules/typescript": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-7.0.2.tgz", - "integrity": "sha512-8FYau96o3NKOhbjKi/qNvG/W5jhzxkbdm5sj9AbZ/5T5sWqn3hJgLfGx27sRKZWTvyzCP8dLRBTf5tBTSRVUNA==", + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-6.0.3.tgz", + "integrity": "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==", "dev": true, "license": "Apache-2.0", "bin": { - "tsc": "bin/tsc" + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" }, "engines": { - "node": ">=16.20.0" - }, - "optionalDependencies": { - "@typescript/typescript-aix-ppc64": "7.0.2", - "@typescript/typescript-darwin-arm64": "7.0.2", - "@typescript/typescript-darwin-x64": "7.0.2", - "@typescript/typescript-freebsd-arm64": "7.0.2", - "@typescript/typescript-freebsd-x64": "7.0.2", - "@typescript/typescript-linux-arm": "7.0.2", - "@typescript/typescript-linux-arm64": "7.0.2", - "@typescript/typescript-linux-loong64": "7.0.2", - "@typescript/typescript-linux-mips64el": "7.0.2", - "@typescript/typescript-linux-ppc64": "7.0.2", - "@typescript/typescript-linux-riscv64": "7.0.2", - "@typescript/typescript-linux-s390x": "7.0.2", - "@typescript/typescript-linux-x64": "7.0.2", - "@typescript/typescript-netbsd-arm64": "7.0.2", - "@typescript/typescript-netbsd-x64": "7.0.2", - "@typescript/typescript-openbsd-arm64": "7.0.2", - "@typescript/typescript-openbsd-x64": "7.0.2", - "@typescript/typescript-sunos-x64": "7.0.2", - "@typescript/typescript-win32-arm64": "7.0.2", - "@typescript/typescript-win32-x64": "7.0.2" + "node": ">=14.17" } }, "node_modules/undici-types": { - "version": "7.18.2", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.18.2.tgz", - "integrity": "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==", + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", "dev": true, "license": "MIT" } diff --git a/package.json b/package.json index 55b48c1..7c4dc15 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "brandc", - "version": "0.6.1", + "version": "0.7.0", "description": "Brand compiler: author a Brand as data and emit the shared design-token contract as CSS (toCss), Tailwind v4 (toTailwindCss) and prefab wire (toPrefabTheme). Ships an example brand you can use as-is or override.", "type": "module", "license": "MIT", @@ -44,7 +44,7 @@ "check": "tsc --noEmit && npm run build && node --test" }, "devDependencies": { - "@types/node": "^24.0.0", - "typescript": "^7.0.2" + "@types/node": "^22.20.1", + "typescript": "^6.0.3" } } diff --git a/scripts/changelog-release.mjs b/scripts/changelog-release.mjs index 97b0822..90cb3e4 100644 --- a/scripts/changelog-release.mjs +++ b/scripts/changelog-release.mjs @@ -2,34 +2,54 @@ // nothing is documented there. Mirrors the release gate used across the org (see prefab). // // node scripts/changelog-release.mjs +// +// Sections are found by index rather than one lookahead regex. The previous +// `/## \[Unreleased\]\s*\n([\s\S]*?)(?=\n## \[|$)/` let `\s*\n` eat the newline the lookahead +// needed, so an EMPTY [Unreleased] captured the next release's section instead — non-empty, so +// the guard never fired, and the reinsert hid it. That shipped 0.6.1 with no notes. import { readFileSync, writeFileSync } from "node:fs"; import { dirname, join } from "node:path"; import { fileURLToPath } from "node:url"; -const version = process.argv[2]; -if (!version) { - console.error("usage: node scripts/changelog-release.mjs "); - process.exit(1); -} +/** A `## [...]` heading at the start of a line. */ +const HEADING = /^## \[/m; -const root = join(dirname(fileURLToPath(import.meta.url)), ".."); -const path = join(root, "CHANGELOG.md"); -const md = readFileSync(path, "utf8"); +/** + * Promote `[Unreleased]` to `[version]`, dated today. Returns the new document. + * Throws when there is no section or nothing documented in it. + */ +export function promoteChangelog(md, version, date) { + const start = md.match(/^## \[Unreleased\][^\n]*\n/m); + if (!start) throw new Error("CHANGELOG.md: no [Unreleased] section found."); -// Capture the [Unreleased] body up to the next "## [" heading (or end of file). -const unreleased = /## \[Unreleased\]\s*\n([\s\S]*?)(?=\n## \[|$)/; -const match = md.match(unreleased); -if (!match) { - console.error("CHANGELOG.md: no [Unreleased] section found."); - process.exit(1); -} -if (!match[1].trim()) { - console.error("Refusing to release: [Unreleased] is empty — document the changes first."); - process.exit(1); + const bodyAt = start.index + start[0].length; + const rest = md.slice(bodyAt); + // Body runs to the next heading; the search is on `rest`, so it can never reach past it. + const nextAt = rest.search(HEADING); + const body = nextAt === -1 ? rest : rest.slice(0, nextAt); + const tail = nextAt === -1 ? "" : rest.slice(nextAt); + + if (!body.trim()) { + throw new Error("Refusing to release: [Unreleased] is empty — document the changes first."); + } + + return `${md.slice(0, start.index)}## [Unreleased]\n\n## [${version}] — ${date}\n\n${body.trim()}\n\n${tail}`; } -const date = new Date().toISOString().slice(0, 10); -const promoted = - `## [Unreleased]\n\n## [${version}] — ${date}\n\n${match[1].trim()}\n`; -writeFileSync(path, md.replace(unreleased, promoted), "utf8"); -console.log(`CHANGELOG.md: [Unreleased] → [${version}] (${date})`); +// Run as a script (not when imported by a test). +if (process.argv[1] && import.meta.url === new URL(`file://${process.argv[1].replace(/\\/g, "/")}`).href) { + const version = process.argv[2]; + if (!version) { + console.error("usage: node scripts/changelog-release.mjs "); + process.exit(1); + } + const path = join(dirname(fileURLToPath(import.meta.url)), "..", "CHANGELOG.md"); + const date = new Date().toISOString().slice(0, 10); + try { + writeFileSync(path, promoteChangelog(readFileSync(path, "utf8"), version, date), "utf8"); + } catch (err) { + console.error(err.message); + process.exit(1); + } + console.log(`CHANGELOG.md: [Unreleased] → [${version}] (${date})`); +} diff --git a/src/contract.ts b/src/contract.ts index 04e1316..a5db2d5 100644 --- a/src/contract.ts +++ b/src/contract.ts @@ -86,10 +86,18 @@ export const CONTRACT_COLORS = [ /** The scheme-independent half: shape, elevation, typography and rhythm. One value, both schemes. */ export const CONTRACT_SCALARS = [ + // Shape. The full Tailwind radius ladder, not a prefix of it: a step the brand does not + // define falls through to Tailwind's own default, so a flat brand that stops at `lg` still + // renders `rounded-xl` at 0.75rem. Every consumer then re-zeroes the tail by hand, which is + // the drift this contract exists to prevent. "radius", "radius-sm", "radius-md", "radius-lg", + "radius-xl", + "radius-2xl", + "radius-3xl", + "radius-4xl", "shadow-sm", "shadow", "shadow-md", diff --git a/src/tokens.ts b/src/tokens.ts index 553c86a..dbfa721 100644 --- a/src/tokens.ts +++ b/src/tokens.ts @@ -131,6 +131,10 @@ export const maxhealth = { "radius-sm": "0rem", "radius-md": "0rem", "radius-lg": "0rem", + "radius-xl": "0rem", + "radius-2xl": "0rem", + "radius-3xl": "0rem", + "radius-4xl": "0rem", "shadow-sm": "none", shadow: "none", "shadow-md": "none", @@ -225,6 +229,10 @@ export const dashboard = { "radius-sm": "0.375rem", "radius-md": "0.5rem", "radius-lg": "0.75rem", + "radius-xl": "1rem", + "radius-2xl": "1.5rem", + "radius-3xl": "2rem", + "radius-4xl": "2.5rem", "shadow-sm": "0 1px 2px 0 rgb(15 23 42 / 0.06)", shadow: "0 1px 3px 0 rgb(15 23 42 / 0.1), 0 1px 2px -1px rgb(15 23 42 / 0.1)", "shadow-md": "0 4px 6px -1px rgb(15 23 42 / 0.1), 0 2px 4px -2px rgb(15 23 42 / 0.1)", diff --git a/tailwind.css b/tailwind.css index 622eed9..3e16418 100644 --- a/tailwind.css +++ b/tailwind.css @@ -58,6 +58,10 @@ --radius-sm: var(--radius-sm); --radius-md: var(--radius-md); --radius-lg: var(--radius-lg); + --radius-xl: var(--radius-xl); + --radius-2xl: var(--radius-2xl); + --radius-3xl: var(--radius-3xl); + --radius-4xl: var(--radius-4xl); --shadow-sm: var(--shadow-sm); --shadow: var(--shadow); --shadow-md: var(--shadow-md); diff --git a/test/changelog.test.mjs b/test/changelog.test.mjs new file mode 100644 index 0000000..bc4a131 --- /dev/null +++ b/test/changelog.test.mjs @@ -0,0 +1,46 @@ +/** + * The release gate that did not gate. An empty [Unreleased] silently captured the NEXT release's + * section, so the "refuse to release undocumented changes" check passed and 0.6.1 shipped with + * no notes. A guard that fails open is worse than no guard, so its behaviour is pinned here. + */ +import assert from "node:assert/strict"; +import test from "node:test"; +import { promoteChangelog } from "../scripts/changelog-release.mjs"; + +const HEAD = `# Changelog\n\nFormat: Keep a Changelog.\n\n`; +const PRIOR = `## [0.6.0] — 2026-07-31\n\n### Fixed\n\n- Something real.\n`; + +test("refuses to release when [Unreleased] is empty", () => { + const md = `${HEAD}## [Unreleased]\n\n${PRIOR}`; + assert.throws(() => promoteChangelog(md, "0.6.1", "2026-08-08"), /\[Unreleased\] is empty/); +}); + +test("an empty [Unreleased] never swallows the previous release's section", () => { + // The exact defect: the old regex captured "## [0.6.0] …" as the Unreleased body. + const md = `${HEAD}## [Unreleased]\n\n${PRIOR}`; + assert.throws(() => promoteChangelog(md, "0.6.1", "2026-08-08")); + // And the prior section is still intact in the source it refused to touch. + assert.match(md, /## \[0\.6\.0\] — 2026-07-31/); +}); + +test("promotes documented changes and keeps the history below", () => { + const md = `${HEAD}## [Unreleased]\n\n### Added\n\n- A thing.\n\n${PRIOR}`; + const out = promoteChangelog(md, "0.7.0", "2026-08-08"); + assert.match(out, /## \[Unreleased\]\n\n## \[0\.7\.0\] — 2026-08-08\n\n### Added\n\n- A thing\./); + assert.match(out, /## \[0\.6\.0\] — 2026-07-31/, "previous release survived"); + assert.equal(out.match(/## \[0\.7\.0\]/g).length, 1, "version stamped exactly once"); +}); + +test("leaves an empty [Unreleased] heading behind for the next cycle", () => { + const out = promoteChangelog(`${HEAD}## [Unreleased]\n\n- Fixed a thing.\n\n${PRIOR}`, "0.7.0", "2026-08-08"); + assert.match(out, /## \[Unreleased\]\n\n## \[0\.7\.0\]/); +}); + +test("works when [Unreleased] is the only section", () => { + const out = promoteChangelog(`${HEAD}## [Unreleased]\n\n- First release.\n`, "0.1.0", "2026-08-08"); + assert.match(out, /## \[0\.1\.0\] — 2026-08-08\n\n- First release\./); +}); + +test("fails loudly when there is no [Unreleased] section at all", () => { + assert.throws(() => promoteChangelog(`${HEAD}${PRIOR}`, "0.7.0", "2026-08-08"), /no \[Unreleased\] section/); +}); diff --git a/test/contract.test.mjs b/test/contract.test.mjs index f0646ce..aaf34ff 100644 --- a/test/contract.test.mjs +++ b/test/contract.test.mjs @@ -28,8 +28,8 @@ test("the contract is a pinned, declared list — changing it is a deliberate ac // A published vocabulary must not drift as a side effect of editing a brand. Re-deriving it // from `maxhealth` (the old defect) puts that brand's two private tokens back and trips this. assert.equal(CONTRACT_COLORS.length, 52, "contract colour count changed — intentional?"); - assert.equal(CONTRACT_SCALARS.length, 26, "contract scalar count changed — intentional?"); - assert.equal(CONTRACT.length, 78); + assert.equal(CONTRACT_SCALARS.length, 30, "contract scalar count changed — intentional?"); + assert.equal(CONTRACT.length, 82); }); test("no contract token is named after a brand", () => { diff --git a/theme.css b/theme.css index 110af08..5d384c3 100644 --- a/theme.css +++ b/theme.css @@ -165,6 +165,10 @@ --radius-sm: 0rem; --radius-md: 0rem; --radius-lg: 0rem; + --radius-xl: 0rem; + --radius-2xl: 0rem; + --radius-3xl: 0rem; + --radius-4xl: 0rem; --shadow-sm: none; --shadow: none; --shadow-md: none;