Skip to content

Release new version - #2

Merged
allxsmith merged 1 commit into
mainfrom
changeset-release/main
May 24, 2025
Merged

allxsmith merged 1 commit into
mainfrom
changeset-release/main

Conversation

@github-actions

Copy link
Copy Markdown
Contributor

This PR was opened by the Changesets release GitHub action. When you're ready to do a release, you can merge this and the packages will be published to npm automatically. If you're not ready to do a release yet, that's fine, whenever you add more changesets to main, this PR will be updated.

Releases

@allxsmith/bestax-lib@1.0.22

Patch Changes

  • b22bcc1: Testing changeset with signed commits for github bot

@allxsmith

Copy link
Copy Markdown
Owner

Test changeset

@github-actions

Copy link
Copy Markdown
Contributor Author

🎉 This PR is included in version 1.0.0 🎉

The release is available on:

Your semantic-release bot 📦🚀

allxsmith added a commit that referenced this pull request Jul 28, 2026
…ore than className

Addresses deep-review advisory #2 on #365. `TabProps` (Tabs.tsx:282-294) takes
`index`, `disabled`, `icon`, `iconLibrary`, `iconVariant`, `iconSize`, and
`iconFeatures` on top of `className` + HTML attributes, so "take only `className`
+ HTML attributes" was imprecise at all four sites. The load-bearing claim — no
Bulma helper props on `Card.*`/`Modal.*`/`Tabs.*`/`Message.*` — is unchanged.

Also surfaces `Tabs.Tab`'s built-in icon props, which the old wording implied did
not exist and would have led an agent to nest an `<Icon>` there.
allxsmith added a commit that referenced this pull request Jul 29, 2026
…AUDE.md, and catalog (#365)

* feat(create-bestax): agent-validated guidance for skills, scaffold CLAUDE.md, and catalog

Distilled from a 10-iteration cold-start eval loop (baseline 85/100 ->
revised mean 95.2, builder cost -43%). Every fact verified against
bulma-ui source before writing; every change validated by at least one
subsequent cold-start build. Full evidence on branch
chore/skill-improvement-loop (experiment/skill-loop/report.md).

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>

* fix(create-bestax): correct the featured-card ring recipe to override --bulma-shadow

The recipe scoped --bulma-box-shadow (and named --bulma-card-shadow as
equally reachable), but .box/.card re-declare those on their own selector,
so an ancestor Theme never wins — verified in a browser: the ring did not
render for either component. Overriding the upstream --bulma-shadow token
does work for both. This also restores consistency with the rule already
stated in bestax-theming/references/css-variables.md.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>

* fix(create-bestax): correct spine, Field, isLight, and Box/Input color guidance

Addresses the CodeRabbit review on #365. Each finding re-verified against source:

- Component spine: the shorthand said "spread `...rest`" without naming
  `useBulmaClasses`, leaving `rest` undefined for a reader — spreading the raw
  props instead leaks helper props onto the DOM and emits none of their classes.
  Now names the hook at all three sites (scaffold CLAUDE.md, catalog generator,
  SKILL.md prose) to match the working template.
- `Field` does accept `color` (Field.tsx:42); only `message`/`messageColor` are
  absent. Corrected the claim.
- `LinkButtonProps` explicitly omits `isLight` (LinkButton.tsx:13), so drop
  LinkButton from the isLight claim; note it on the reference row too.
- Box/Card/Section emit no `is-<color>` rule — their `color` flows into
  `useBulmaClasses` as the text helper, so lumping them with Button/Hero as
  "modifier displaced the helper" was wrong. (CodeRabbit flagged Box; Card and
  Section have the same shape.)
- `Input` renders a native `<input>`, so the `<Span textColor>` wrapper cannot
  color its value. Bulma declares `--bulma-input-*` on `.input` itself, so an
  ancestor `<Theme>` can't reach them either — point at the upstream
  `--bulma-text-strong-l`, same rule as the shadow fix in 71c03bc.
- stat-card's icon is decorative next to a visible label; `ariaLabel` made it
  announce twice. Now `aria-hidden="true"`, matching bestax-icons' own guidance.

Not changed: the decorative-CSS example is exactly 10 lines, within the stated
"≤10 lines" budget — the reported 11-line overrun does not reproduce.

* fix(create-bestax): state sub-part props precisely — Tabs.Tab takes more than className

Addresses deep-review advisory #2 on #365. `TabProps` (Tabs.tsx:282-294) takes
`index`, `disabled`, `icon`, `iconLibrary`, `iconVariant`, `iconSize`, and
`iconFeatures` on top of `className` + HTML attributes, so "take only `className`
+ HTML attributes" was imprecise at all four sites. The load-bearing claim — no
Bulma helper props on `Card.*`/`Modal.*`/`Tabs.*`/`Message.*` — is unchanged.

Also surfaces `Tabs.Tab`'s built-in icon props, which the old wording implied did
not exist and would have led an agent to nest an `<Icon>` there.

* fix(create-bestax): note that Field's color prop is typed but inert

Addresses the deep-review advisory on #365. The previous wording ("Field accepts
`color` too") was type-true but behavior-false: `FieldComponent` destructures
`color: _fieldColor` and never uses it (Field.tsx:189), so it reaches neither
`useBulmaClasses` nor the DOM and renders no class.

Note this cuts against the CodeRabbit suggestion that prompted the earlier
edit — `FieldProps` does declare `color`, but declaring it is not honoring it.
The doc now states the practical truth (put validation state on the input) and
the API truth (the prop exists and does nothing).

Also verified the neighbors, which differ: `FieldLabel` and `FieldBody` do *not*
destructure `color`, so it flows through `...props` into `useBulmaClasses` and
lands as the `has-text-*` helper. Called out so the three aren't assumed alike.

---------

Co-authored-by: Claude <noreply@anthropic.com>
github-actions Bot pushed a commit that referenced this pull request Jul 29, 2026
# [3.8.0](https://github.com/allxsmith/bestax/compare/create-bestax@3.7.1...create-bestax@3.8.0) (2026-07-29)

### Features

* **create-bestax:** agent-validated guidance for skills, scaffold CLAUDE.md, and catalog ([#365](#365)) ([6fd06ae](6fd06ae)), closes [#2](#2)
bestax-release-bot Bot pushed a commit that referenced this pull request Aug 1, 2026
# [2.0.0](https://github.com/allxsmith/bestax/compare/bestax-migrate@1.0.0...bestax-migrate@2.0.0) (2026-08-01)

### Bug Fixes

* **bestax-migrate:** give the kitchen-sink e2e a per-process scratch dir ([2211ea5](2211ea5))
* **bestax-migrate:** reject pnpm's workspace alias form instead of unwrapping it ([de6a900](de6a900))
* **bestax-migrate:** require the pack script to exist, not just be named ([5315efe](5315efe))
* **bestax-migrate:** resolve bare workspace: and guard the catalog: protocol ([7fda9db](7fda9db)), closes [#417](#417) [#412](#412)
* **bestax-migrate:** resolve workspace: specifiers before publishing ([782829a](782829a)), closes [bestax-migrate#test](https://github.com/bestax-migrate/issues/test) [#412](#412)
* **bestax-migrate:** stop the pack hooks excusing a catalog: devDependency ([4127ead](4127ead)), closes [#412-shaped](#412)
* **create-bestax:** concrete inline-style → helper-prop mapping for the never-inline rule ([#357](#357)) ([5f72a90](5f72a90)), closes [#350](#350) [#350](#350)
* **docs:** stop cssnano stripping Font Awesome [@font-face](https://github.com/font-face), add [#3](#3) CSS framework blog post ([#401](#401)) ([5d114e1](5d114e1)), closes [#400](#400)

### chore

* **deps:** consolidate the dependabot backlog, require Node 22 in both CLIs ([#447](#447)) ([e68148c](e68148c)), closes [#427](#427) [#428](#428) [#431](#431) [#432](#432) [#440](#440) [#393](#393)

### Features

* **bestax-migrate:** require Node 22 and take chalk 6 ([#449](#449)) ([4c0e1e2](4c0e1e2)), closes [#447](#447)
* **bulma-ui:** ship agent-discovery files in the npm tarball ([#345](#345)) ([4b58739](4b58739)), closes [#344](#344) [#344](#344) [#344](#344)
* **create-bestax:** add controlled-Burger Navbar to the landing archetype ([#355](#355)) ([36d4d09](36d4d09)), closes [#348](#348)
* **create-bestax:** agent-validated guidance for skills, scaffold CLAUDE.md, and catalog ([#365](#365)) ([6fd06ae](6fd06ae)), closes [#2](#2)
* **create-bestax:** require Node 22 and take chalk 6 ([#448](#448)) ([90fced2](90fced2)), closes [#447](#447)
* **create-bestax:** scaffold .claude/launch.json with the AI skills opt-in ([#343](#343)) ([189135a](189135a))
* **create-bestax:** set scaffolded index.html title to the project name ([#356](#356)) ([3bfbea3](3bfbea3)), closes [#349](#349) [#349](#349)

### BREAKING CHANGES

* **bestax-migrate:** bestax-migrate now requires Node.js 22 or newer. Node 18 and
20 are both past end-of-life. Running it on an older runtime prints an explicit
upgrade message and exits 1. This applies to the runtime the codemod executes
on, not to the app being migrated.

Claude-Session: https://claude.ai/code/session_01TGA6sFTUGsJ6oXhfpjKEnh
* **create-bestax:** create-bestax now requires Node.js 22 or newer. Node 18 and 20
are both past end-of-life. Running it on an older runtime prints an explicit
upgrade message and exits 1.

Claude-Session: https://claude.ai/code/session_01TGA6sFTUGsJ6oXhfpjKEnh
* **deps:** create-bestax now requires Node.js 22 or newer. Node 18 and 20
are both past end-of-life. Running it on an older runtime prints an explicit
upgrade message and exits 1.

Claude-Session: https://claude.ai/code/session_01TGA6sFTUGsJ6oXhfpjKEnh

* feat(bestax-migrate): require Node 22 and take chalk 6

chalk 6 drops support for Node below 22. The API surface this package uses is
unchanged, so no calling code changes.

The version guard in src/index.ts moves ahead of every import and no longer
depends on anything: import declarations are hoisted and evaluated before any
statement in the module, and chalk 6 itself requires Node >= 22, so a static
import would fail to load on exactly the runtimes the guard exists to catch.
./cli.js is now imported dynamically for the same reason.

@babel/parser deliberately stays on 7.x. Babel 8 removes the
`deprecatedImportAssert` plugin with no replacement, and this package parses
the legacy `import x from 'y' assert { type: 'json' }` form on purpose — a
codemod that migrates older codebases must not crash on the syntax those
codebases still contain. There is a regression test for it ("parses the legacy
import-assert syntax"), which Babel 8 fails outright. jscodeshift 17 bundles
its own Babel 7 regardless, so staying on 7 also keeps a single parser in the
tree rather than two.
* **deps:** bestax-migrate now requires Node.js 22 or newer. Node 18 and
20 are both past end-of-life. Running it on an older runtime prints an explicit
upgrade message and exits 1.

Claude-Session: https://claude.ai/code/session_01TGA6sFTUGsJ6oXhfpjKEnh
bestax-release-bot Bot pushed a commit that referenced this pull request Aug 7, 2026
## [5.8.1](https://github.com/allxsmith/bestax/compare/@allxsmith/bestax-bulma@5.8.0...@allxsmith/bestax-bulma@5.8.1) (2026-08-07)

### Bug Fixes

* **bestax-migrate:** give the kitchen-sink e2e a per-process scratch dir ([2211ea5](2211ea5))
* **bestax-migrate:** reject pnpm's workspace alias form instead of unwrapping it ([de6a900](de6a900))
* **bestax-migrate:** require the pack script to exist, not just be named ([5315efe](5315efe))
* **bestax-migrate:** resolve bare workspace: and guard the catalog: protocol ([7fda9db](7fda9db)), closes [#417](#417) [#412](#412)
* **bestax-migrate:** resolve workspace: specifiers before publishing ([782829a](782829a)), closes [bestax-migrate#test](https://github.com/bestax-migrate/issues/test) [#412](#412)
* **bestax-migrate:** stop the pack hooks excusing a catalog: devDependency ([4127ead](4127ead)), closes [#412-shaped](#412)
* **bulma-ui:** deprecate CSS-less color values, warn in dev, fix has-text fall-through ([fb111eb](fb111eb))
* **bulma-ui:** fail closed on missing process and scope color guidance to real props ([117c0c0](117c0c0))
* **create-bestax:** concrete inline-style → helper-prop mapping for the never-inline rule ([#357](#357)) ([5f72a90](5f72a90)), closes [#350](#350) [#350](#350)
* **create-bestax:** validate at submit in the bestax-form signup example ([0b9518f](0b9518f))
* **create-bestax:** wire labeled controls in the skill showcase story ([af49a16](af49a16))
* **docs:** announce the hero copy, and stop remounting the icons ([98e2cb0](98e2cb0)), closes [#434](#434)
* **docs:** correct the frozen-install translation and reject leaked fences ([1883de3](1883de3))
* **docs:** drop dead nomodule ionicons fallback ([82be3e4](82be3e4))
* **docs:** harden PackageManagerTabs and document how to author it ([5b0d3e6](5b0d3e6)), closes [#434](#434)
* **docs:** harden the hero copy button and share the tab storage key ([9e16cd7](9e16cd7))
* **docs:** make the hero package-manager switcher a real radiogroup ([aa14ff2](aa14ff2)), closes [#434](#434)
* **docs:** stop cssnano stripping Font Awesome [@font-face](https://github.com/font-face), add [#3](#3) CSS framework blog post ([#401](#401)) ([5d114e1](5d114e1)), closes [#400](#400)

### chore

* **deps:** consolidate the dependabot backlog, require Node 22 in both CLIs ([#447](#447)) ([e68148c](e68148c)), closes [#427](#427) [#428](#428) [#431](#431) [#432](#432) [#440](#440) [#393](#393)

### Features

* **bestax-migrate:** require Node 22 and take chalk 6 ([#449](#449)) ([4c0e1e2](4c0e1e2)), closes [#447](#447)
* **create-bestax:** add controlled-Burger Navbar to the landing archetype ([#355](#355)) ([36d4d09](36d4d09)), closes [#348](#348)
* **create-bestax:** agent-validated guidance for skills, scaffold CLAUDE.md, and catalog ([#365](#365)) ([6fd06ae](6fd06ae)), closes [#2](#2)
* **create-bestax:** require Node 22 and take chalk 6 ([#448](#448)) ([90fced2](90fced2)), closes [#447](#447)
* **create-bestax:** set scaffolded index.html title to the project name ([#356](#356)) ([3bfbea3](3bfbea3)), closes [#349](#349) [#349](#349)
* **docs:** add package-manager switches to the homepage hero ([374caf8](374caf8))
* **docs:** add PackageManagerTabs and register it globally ([23c9989](23c9989))
* **docs:** show all posts in the blog sidebar ([d29e9c6](d29e9c6))

### Performance Improvements

* **docs:** defer live previews until they scroll into view ([d6bf87b](d6bf87b))
* **docs:** share one parsed stylesheet set across every live preview ([feb993a](feb993a))

### BREAKING CHANGES

* **bestax-migrate:** bestax-migrate now requires Node.js 22 or newer. Node 18 and
20 are both past end-of-life. Running it on an older runtime prints an explicit
upgrade message and exits 1. This applies to the runtime the codemod executes
on, not to the app being migrated.

Claude-Session: https://claude.ai/code/session_01TGA6sFTUGsJ6oXhfpjKEnh
* **create-bestax:** create-bestax now requires Node.js 22 or newer. Node 18 and 20
are both past end-of-life. Running it on an older runtime prints an explicit
upgrade message and exits 1.

Claude-Session: https://claude.ai/code/session_01TGA6sFTUGsJ6oXhfpjKEnh
* **deps:** create-bestax now requires Node.js 22 or newer. Node 18 and 20
are both past end-of-life. Running it on an older runtime prints an explicit
upgrade message and exits 1.

Claude-Session: https://claude.ai/code/session_01TGA6sFTUGsJ6oXhfpjKEnh

* feat(bestax-migrate): require Node 22 and take chalk 6

chalk 6 drops support for Node below 22. The API surface this package uses is
unchanged, so no calling code changes.

The version guard in src/index.ts moves ahead of every import and no longer
depends on anything: import declarations are hoisted and evaluated before any
statement in the module, and chalk 6 itself requires Node >= 22, so a static
import would fail to load on exactly the runtimes the guard exists to catch.
./cli.js is now imported dynamically for the same reason.

@babel/parser deliberately stays on 7.x. Babel 8 removes the
`deprecatedImportAssert` plugin with no replacement, and this package parses
the legacy `import x from 'y' assert { type: 'json' }` form on purpose — a
codemod that migrates older codebases must not crash on the syntax those
codebases still contain. There is a regression test for it ("parses the legacy
import-assert syntax"), which Babel 8 fails outright. jscodeshift 17 bundles
its own Babel 7 regardless, so staying on 7 also keeps a single parser in the
tree rather than two.
* **deps:** bestax-migrate now requires Node.js 22 or newer. Node 18 and
20 are both past end-of-life. Running it on an older runtime prints an explicit
upgrade message and exits 1.

Claude-Session: https://claude.ai/code/session_01TGA6sFTUGsJ6oXhfpjKEnh
bestax-release-bot Bot pushed a commit that referenced this pull request Aug 12, 2026
# 1.0.0 (2026-08-12)

* feat(bulma-ui)!: remove bestax-bulma-prefixed CSS variant ([94baa34](94baa34))
* feat(create-bestax)!: require Node.js 18+ and align with bestax-bulma v2 ([#118](#118)) ([b22f183](b22f183))

### Bug Fixes

* add comprehensive rules to prevent bulma-ui versioning on non-bulma-ui commits ([#122](#122)) ([525ccfa](525ccfa)), closes [#119](#119)
* **bestax-mcp:** derive the near-miss guidance from the skill, and only when it helps ([1141cca](1141cca))
* **bestax-mcp:** do not split a helper-prop table cell on an escaped pipe ([bdac820](bdac820))
* **bestax-mcp:** lead get_helper_props with the inline-style prohibition ([ffc627a](ffc627a))
* **bestax-mcp:** make list_components point at the next step ([8ddb2fd](8ddb2fd))
* **bestax-mcp:** make tests and cached builds work from a clean checkout ([6e63820](6e63820)), closes [bestax-mcp#build](https://github.com/bestax-mcp/issues/build)
* **bestax-mcp:** name list_components as the entry point, not search_bestax ([206380b](206380b))
* **bestax-mcp:** name the three near-miss components in the list_components footer ([1c7af67](1c7af67))
* **bestax-mcp:** route helper questions to the tool that answers them ([cd6ce12](cd6ce12))
* **bestax-mcp:** validate the one input that is not ours, and bound the rest ([3e1adc9](3e1adc9))
* **bestax-migrate:** give the kitchen-sink e2e a per-process scratch dir ([2211ea5](2211ea5))
* **bestax-migrate:** reject pnpm's workspace alias form instead of unwrapping it ([de6a900](de6a900))
* **bestax-migrate:** require the pack script to exist, not just be named ([5315efe](5315efe))
* **bestax-migrate:** resolve bare workspace: and guard the catalog: protocol ([7fda9db](7fda9db)), closes [#417](#417) [#412](#412)
* **bestax-migrate:** resolve workspace: specifiers before publishing ([782829a](782829a)), closes [bestax-migrate#test](https://github.com/bestax-migrate/issues/test) [#412](#412)
* **bestax-migrate:** stop the pack hooks excusing a catalog: devDependency ([4127ead](4127ead)), closes [#412-shaped](#412)
* **bulma-ui:** a11y + case-insensitive Taginput matching from PR review ([d576829](d576829))
* **bulma-ui:** accept router props like `to` on Navbar.Item without casts ([#311](#311)) ([b78856b](b78856b)), closes [#306](#306)
* **bulma-ui:** Add build step to publish in ci.yml ([e3707fc](e3707fc))
* **bulma-ui:** add fontawesome-free as explicit devDependency ([a4a5389](a4a5389))
* **bulma-ui:** add missing exports ([0d16633](0d16633))
* **bulma-ui:** Add Skeleton to exports ([e481599](e481599))
* **bulma-ui:** another attempt to fix semantic release builds with ci.yml ([cc3a3e2](cc3a3e2))
* **bulma-ui:** another attempt to fix semantic release builds with ci.yml ([314bc39](314bc39))
* **bulma-ui:** another attempt to fix semantic release builds with ci.yml ([c930693](c930693))
* **bulma-ui:** associate Autocomplete and Taginput labels with their inner inputs ([7ae37d4](7ae37d4))
* **bulma-ui:** associate Autocomplete and Taginput labels with their inner inputs ([384bd38](384bd38))
* **bulma-ui:** associate the form label prop with its control via a generated id ([e6686af](e6686af))
* **bulma-ui:** complete domain migration and fix semantic-release configuration ([#64](#64)) ([f4cd71d](f4cd71d))
* **bulma-ui:** correct blog post examples and add Modal compound components ([#81](#81)) ([559c2e3](559c2e3))
* **bulma-ui:** correct NPM_TOKEN env variable in ci.yml ([94b48b4](94b48b4))
* **bulma-ui:** cover horizontal-layout group label association ([ef3ca9f](ef3ca9f))
* **bulma-ui:** deprecate CSS-less color values, warn in dev, fix has-text fall-through ([fb111eb](fb111eb))
* **bulma-ui:** fail closed on missing process and scope color guidance to real props ([117c0c0](117c0c0))
* **bulma-ui:** Fix release.config.js to include package-lock.json ([390da59](390da59))
* **bulma-ui:** fix standalone Badge pointer-events, pulse halo, and falsy content ([#295](#295)) ([a9db031](a9db031)), closes [#264](#264)
* **bulma-ui:** full classPrefix support across layout/grid + prefix utils ([4ce0b53](4ce0b53))
* **bulma-ui:** honor the htmlFor opt-out in the convenience hook and tighten the association docs ([92aa622](92aa622))
* **bulma-ui:** improve npm package discoverability with optimized keywords and badges ([#72](#72)) ([8c7a696](8c7a696))
* **bulma-ui:** Initial semantic release changes ([b78d785](b78d785))
* **bulma-ui:** keep Taginput's fallback name unless the label targets its input ([73cec33](73cec33))
* **bulma-ui:** keep Taginput's fallback name unless the label targets its input ([ca5996a](ca5996a))
* **bulma-ui:** migrate domain from bestax.cc to bestax.io ([#64](#64)) ([4870b1e](4870b1e))
* **bulma-ui:** migrate ionicons to v8 to unblock publish and Storybook ([927a55b](927a55b)), closes [#142](#142)
* **bulma-ui:** name Rate, Checkboxes, and Radios groups from their labels via aria-labelledby ([dce0ee7](dce0ee7))
* **bulma-ui:** name Rate, Checkboxes, and Radios groups from their labels via aria-labelledby ([#497](#497)) ([5c4222e](5c4222e))
* **bulma-ui:** name the three near-miss components in AGENTS.md ([c63f491](c63f491)), closes [#344](#344)
* **bulma-ui:** never let labelProps.htmlFor wire a group label to a control ([3b3aaaf](3b3aaaf))
* **bulma-ui:** publish rewritten README to npm ([9810081](9810081))
* **bulma-ui:** publish with npm provenance attestation ([172da62](172da62)), closes [#180](#180)
* **bulma-ui:** reference llms docs from README and package.json ([#198](#198)) ([db8aab3](db8aab3))
* **bulma-ui:** reject predicate-blocked values during manual entry ([a8f6e28](a8f6e28))
* **bulma-ui:** resolve flex item properties and Card compound component issues ([#55](#55)) ([e774da3](e774da3))
* **bulma-ui:** resolve flex item properties and Card compound component issues ([#55](#55)) ([7641a53](7641a53))
* **bulma-ui:** resolve react-hooks v7 and [@eslint-react](https://github.com/eslint-react) findings ([14caaaf](14caaaf))
* **bulma-ui:** resolve security vulnerabilities and update dependencies ([#128](#128)) ([112f6e4](112f6e4)), closes [#127](#127)
* **bulma-ui:** restrict semantic-release to bulma-ui scoped commits only ([2d67bf9](2d67bf9)), closes [#62](#62)
* **bulma-ui:** retry failed Avatar src, flatten Fragment children in Avatars, RTL-safe overlap ([#297](#297)) ([c00b9db](c00b9db))
* **bulma-ui:** route every hardcoded class through the prefix helpers; add classPrefix sweep test ([#301](#301)) ([a50b134](a50b134)), closes [#286](#286)
* **bulma-ui:** setup gpg signing with semantic-release ([3e24722](3e24722))
* **bulma-ui:** strip redundant library prefix from Icon name ([#242](#242)) ([dbe3622](dbe3622)), closes [#189](#189)
* **bulma-ui:** trigger release to publish via OIDC trusted publishing ([e2d09c5](e2d09c5))
* **bulma-ui:** update bundle size claims to accurate 21KB gzipped ([#66](#66)) ([6e381bd](6e381bd))
* **bulma-ui:** update package-lock.json ([853d585](853d585))
* **bulma-ui:** update package.json for better seo, exports, types, engines, funding, etc ([98cbc56](98cbc56))
* **bulma-ui:** use createRequire for ESM compatibility in Storybook 10 ([#130](#130)) ([b27e60e](b27e60e)), closes [#129](#129)
* **ci:** collect screenshots as artifacts and commit in single batch to avoid conflicts ([27b259d](27b259d))
* **ci:** ensure npm install uses fresh downloads with --prefer-online ([1f2e15d](1f2e15d))
* **ci:** properly extract base path for recursive file search ([e0330ff](e0330ff))
* **ci:** use find command instead of glob module in verified-commit action ([0e2d159](0e2d159))
* **ci:** use npm ci for scaffolded app dependencies ([35652c8](35652c8))
* **create-bestax:** concrete inline-style → helper-prop mapping for the never-inline rule ([#357](#357)) ([5f72a90](5f72a90)), closes [#350](#350) [#350](#350)
* **create-bestax:** correct browser title to prioritize Bestax branding ([#106](#106)) ([23aa535](23aa535)), closes [#105](#105)
* **create-bestax:** correct template path resolution from ../../ to ../ ([65b4493](65b4493)), closes [#78](#78)
* **create-bestax:** dark-mode contrast rules in theming/layout skills and docs ([#303](#303)) ([490bf21](490bf21)), closes [#194](#194) [#195](#195)
* **create-bestax:** exclude templates directory from linting and typecheck ([18fec0b](18fec0b))
* **create-bestax:** fail fast with guidance instead of hanging when stdin is not a TTY ([#293](#293)) ([46a172d](46a172d)), closes [#192](#192)
* **create-bestax:** move templates into package directory and update docs ([195bf01](195bf01)), closes [#78](#78)
* **create-bestax:** point scaffolded CLAUDE.md at llms docs; document skills ([#198](#198)) ([b2e0514](b2e0514))
* **create-bestax:** publish with npm provenance attestation ([21ffe8f](21ffe8f)), closes [#180](#180)
* **create-bestax:** put the near-miss guidance where every session sees it ([6db49f3](6db49f3))
* **create-bestax:** read version from package.json instead of hardcoded value ([#109](#109)) ([8605699](8605699))
* **create-bestax:** refresh README and bump scaffolded bestax-bulma to ^5 ([4e19e86](4e19e86))
* **create-bestax:** reject dot-only project names, pin icon versions, bundle bestax-icons skill ([#310](#310)) ([ddff8e5](ddff8e5))
* **create-bestax:** scaffold @allxsmith/bestax-bulma ^4.0.0 ([1d3b802](1d3b802))
* **create-bestax:** scaffold bundled bestax CSS flavors, not stock Bulma ([43621dc](43621dc))
* **create-bestax:** ship improved bundled skills + component catalog ([#199](#199)) ([a1515c2](a1515c2))
* **create-bestax:** shrink the near-miss block and pin the copies together ([d582da5](d582da5))
* **create-bestax:** skills-sync conformance gate + theming skill reference backfill ([#326](#326)) ([9584133](9584133)), closes [#285](#285)
* **create-bestax:** stop the skills teaching a Theme call that does not compile ([2935bb2](2935bb2))
* **create-bestax:** synchronize version with bestax-bulma to 2.4.0 ([623ee79](623ee79)), closes [#96](#96)
* **create-bestax:** teach the skills the three components Bulma hides ([22dcff7](22dcff7))
* **create-bestax:** update template dependency to ^2.4.0 ([200971d](200971d))
* **create-bestax:** use scenario-specific screenshot directories to prevent overwrites ([#108](#108)) ([c675957](c675957)), closes [#107](#107)
* **create-bestax:** validate at submit in the bestax-form signup example ([0b9518f](0b9518f))
* **create-bestax:** wire labeled controls in the skill showcase story ([af49a16](af49a16))
* **docs:** announce the hero copy, and stop remounting the icons ([98e2cb0](98e2cb0)), closes [#434](#434)
* **docs:** correct Content Signals syntax in robots.txt ([#134](#134)) ([85dd9de](85dd9de))
* **docs:** correct the frozen-install translation and reject leaked fences ([1883de3](1883de3))
* **docs:** drop dead nomodule ionicons fallback ([82be3e4](82be3e4))
* **docs:** emit per-page markdown so llms.txt links resolve ([#200](#200)) ([7877083](7877083))
* **docs:** escape apostrophe in QuickStart notification text ([25d6d72](25d6d72))
* **docs:** generate llms.txt so the advertised homepage link resolves ([9fae464](9fae464)), closes [#177](#177)
* **docs:** give every batch run its own port — slot reuse was corrupting runs ([6ef1755](6ef1755))
* **docs:** harden PackageManagerTabs and document how to author it ([5b0d3e6](5b0d3e6)), closes [#434](#434)
* **docs:** harden the hero copy button and share the tab storage key ([9e16cd7](9e16cd7))
* **docs:** improve homepage hero layout and button spacing ([5f7a5a7](5f7a5a7))
* **docs:** make the eval batch resumable after a container restart ([d56229e](d56229e))
* **docs:** make the hero package-manager switcher a real radiogroup ([aa14ff2](aa14ff2)), closes [#434](#434)
* **docs:** move robots.txt to correct deployment location ([#90](#90)) ([1e2aeee](1e2aeee))
* **docs:** rebrand and reorganize Storybook ([#83](#83)) ([dfb9937](dfb9937))
* **docs:** remove Google Analytics and add robots.txt ([94776f7](94776f7))
* **docs:** stop cssnano stripping Font Awesome [@font-face](https://github.com/font-face), add [#3](#3) CSS framework blog post ([#401](#401)) ([5d114e1](5d114e1)), closes [#400](#400)
* **docs:** update Storybook logo path to /img/logo.svg for deployed site ([bf59758](bf59758))
* **e2e:** correct notification CSS selectors to use contains instead of ends-with ([182acc1](182acc1))
* implement independent package versioning strategy ([#111](#111)) ([7819c73](7819c73)), closes [#110](#110)
* prevent bulma-ui from versioning on create-bestax commits ([#120](#120)) ([4dfaf9c](4dfaf9c)), closes [#119](#119)
* resolve React Hooks violations and ESLint configuration issues ([32d2931](32d2931))
* upgrade Turbo, Storybook, and Docusaurus dependencies ([5b4ebdd](5b4ebdd)), closes [#98](#98)

### chore

* **deps:** consolidate the dependabot backlog, require Node 22 in both CLIs ([#447](#447)) ([e68148c](e68148c)), closes [#427](#427) [#428](#428) [#431](#431) [#432](#432) [#440](#440) [#393](#393)

### Documentation

* fix stale versioning and coverage docs; drop CLAUDE.md stale-docs flags ([71c4583](71c4583))

### Features

* add theme system and config provider with comprehensive test coverage ([f3ca7f0](f3ca7f0))
* **bestax-mcp:** serve component docs, props, examples and skills over MCP ([c2abcc4](c2abcc4))
* **bestax-migrate:** react-bulma-components → bestax-bulma codemod CLI, skill, and docs ([#333](#333)) ([e04a12b](e04a12b)), closes [#1e6b99](https://github.com/allxsmith/bestax/issues/1e6b99)
* **bestax-migrate:** require Node 22 and take chalk 6 ([#449](#449)) ([4c0e1e2](4c0e1e2)), closes [#447](#447)
* **bulma-ui:** add Avatar, Avatars, and Badge components ([#257](#257)) ([0817018](0817018)), closes [#256](#256)
* **bulma-ui:** add colorMode dark-mode prop to Theme ([4acc41e](4acc41e)), closes [#174](#174)
* **bulma-ui:** add consistent gap prop to Columns, aliasing gapSize ([#300](#300)) ([6c36455](6c36455)), closes [#282](#282)
* **bulma-ui:** add cursor helper, closeDelay prop, and polish Tooltip stories ([37945b5](37945b5))
* **bulma-ui:** add extra components, form elements, and SCSS styles ([59daf28](59daf28))
* **bulma-ui:** add HTML element wrapper components ([#135](#135)) ([#136](#136)) ([20fb16d](20fb16d))
* **bulma-ui:** add manual-entry stories for format, bounds, and blocked-value variations ([e93d51c](e93d51c))
* **bulma-ui:** add Reveal component for scroll-triggered animations ([#255](#255)) ([a89c574](a89c574))
* **bulma-ui:** Add skeletons ([6c46e4b](6c46e4b))
* **bulma-ui:** add themed Checkbox/Radio, convenience Field components, and Autocomplete cleanup ([3c57a5a](3c57a5a))
* **bulma-ui:** add typing-first story variants for all picker property variations ([078433f](078433f))
* **bulma-ui:** associate Field's label with a composed base control ([219f631](219f631))
* **bulma-ui:** avatar/badge a11y batch — decorative alt, accessible names, live region, button type, surplus i18n, focus ring ([#298](#298)) ([508477f](508477f)), closes [#266](#266) [#266](#266)
* **bulma-ui:** change the default primary color to [#1](#1 ([8872620](8872620)), closes [#1e6b99](https://github.com/allxsmith/bestax/issues/1e6b99) [#1e6b99](https://github.com/allxsmith/bestax/issues/1e6b99)
* **bulma-ui:** compound (dot-notation) sub-components for all parent/child families via shared withSubComponents helper ([#331](#331)) ([07516c5](07516c5))
* **bulma-ui:** dim and blur the calendar behind the Datetimepicker time wheels ([3d90619](3d90619))
* **bulma-ui:** finalize the 3.0 component set ([87ccc0e](87ccc0e))
* **bulma-ui:** make Button and Link as prop polymorphic (React.ElementType) ([#238](#238)) ([ce90304](ce90304)), closes [#188](#188)
* **bulma-ui:** require React 18 as the minimum supported version ([c7251b0](c7251b0))
* **bulma-ui:** ship agent-discovery files in the npm tarball ([#345](#345)) ([4b58739](4b58739)), closes [#344](#344) [#344](#344) [#344](#344)
* **ci:** add verified-commit action for GPG-signed commits ([d078dfa](d078dfa))
* **create-bestax:** add bestax-optimize skill for shrinking built CSS ([#329](#329)) ([f597b9f](f597b9f))
* **create-bestax:** add CLI tool with Vite templates and automated publishing ([9748c3d](9748c3d))
* **create-bestax:** add controlled-Burger Navbar to the landing archetype ([#355](#355)) ([36d4d09](36d4d09)), closes [#348](#348)
* **create-bestax:** add cross-platform emoji support with figures ([#103](#103)) ([15567d9](15567d9))
* **create-bestax:** add README with templates location note ([8ddc73d](8ddc73d))
* **create-bestax:** add visual regression testing and synchronized versioning ([17e1e22](17e1e22)), closes [#94](#94)
* **create-bestax:** agent-validated guidance for skills, scaffold CLAUDE.md, and catalog ([#365](#365)) ([6fd06ae](6fd06ae)), closes [#2](#2)
* **create-bestax:** bestax-icons skill — teach agents the icon system ([#302](#302)) ([61c8ef2](61c8ef2)), closes [#287](#287)
* **create-bestax:** improve favicon visibility and add distinct branding ([621590d](621590d)), closes [#100](#100)
* **create-bestax:** modernize templates (Vite 8, ESLint 10, TS 6) + add working lint config ([4537629](4537629)), closes [#167](#167)
* **create-bestax:** offer to install the bestax AI skills when scaffolding ([625b7bf](625b7bf)), closes [#174](#174)
* **create-bestax:** require Node 22 and take chalk 6 ([#448](#448)) ([90fced2](90fced2)), closes [#447](#447)
* **create-bestax:** scaffold .claude/launch.json with the AI skills opt-in ([#343](#343)) ([189135a](189135a))
* **create-bestax:** scaffold-aware CLAUDE.md with setup facts and house style ([#271](#271)) ([c1681b0](c1681b0))
* **create-bestax:** set scaffolded index.html title to the project name ([#356](#356)) ([3bfbea3](3bfbea3)), closes [#349](#349) [#349](#349)
* **docs:** add Google Analytics tracking for usage insights ([#68](#68)) ([90ab951](90ab951))
* **docs:** add package-manager switches to the homepage hero ([374caf8](374caf8))
* **docs:** add PackageManagerTabs and register it globally ([23c9989](23c9989))
* **docs:** add pronunciation guide and dark mode support ([#58](#58)) ([48a8916](48a8916))
* **docs:** aggregate-runs.mjs — distribution stats across a runs directory ([5de9c07](5de9c07))
* **docs:** batch runner for the eval harness, with the concurrency fixes it needed ([f8e268c](f8e268c))
* **docs:** migrate from GitHub Pages to Cloudflare Pages ([#132](#132)) ([2154672](2154672)), closes [#131](#131)
* **docs:** rubric v2 and a brief that demands the components beyond Bulma ([e6047be](e6047be))
* **docs:** show all posts in the blog sidebar ([d29e9c6](d29e9c6))
* **form:** add Datepicker, Timepicker, and Datetimepicker components ([c6684e6](c6684e6))

### Performance Improvements

* **bestax-mcp:** stop get_helper_props costing half the session ([b865651](b865651))
* **docs:** defer live previews until they scroll into view ([d6bf87b](d6bf87b))
* **docs:** share one parsed stylesheet set across every live preview ([feb993a](feb993a))

### BREAKING CHANGES

* **bestax-migrate:** bestax-migrate now requires Node.js 22 or newer. Node 18 and
20 are both past end-of-life. Running it on an older runtime prints an explicit
upgrade message and exits 1. This applies to the runtime the codemod executes
on, not to the app being migrated.

Claude-Session: https://claude.ai/code/session_01TGA6sFTUGsJ6oXhfpjKEnh
* **create-bestax:** create-bestax now requires Node.js 22 or newer. Node 18 and 20
are both past end-of-life. Running it on an older runtime prints an explicit
upgrade message and exits 1.

Claude-Session: https://claude.ai/code/session_01TGA6sFTUGsJ6oXhfpjKEnh
* **deps:** create-bestax now requires Node.js 22 or newer. Node 18 and 20
are both past end-of-life. Running it on an older runtime prints an explicit
upgrade message and exits 1.

Claude-Session: https://claude.ai/code/session_01TGA6sFTUGsJ6oXhfpjKEnh

* feat(bestax-migrate): require Node 22 and take chalk 6

chalk 6 drops support for Node below 22. The API surface this package uses is
unchanged, so no calling code changes.

The version guard in src/index.ts moves ahead of every import and no longer
depends on anything: import declarations are hoisted and evaluated before any
statement in the module, and chalk 6 itself requires Node >= 22, so a static
import would fail to load on exactly the runtimes the guard exists to catch.
./cli.js is now imported dynamically for the same reason.

@babel/parser deliberately stays on 7.x. Babel 8 removes the
`deprecatedImportAssert` plugin with no replacement, and this package parses
the legacy `import x from 'y' assert { type: 'json' }` form on purpose — a
codemod that migrates older codebases must not crash on the syntax those
codebases still contain. There is a regression test for it ("parses the legacy
import-assert syntax"), which Babel 8 fails outright. jscodeshift 17 bundles
its own Babel 7 regardless, so staying on 7 also keeps a single parser in the
tree rather than two.
* **deps:** bestax-migrate now requires Node.js 22 or newer. Node 18 and
20 are both past end-of-life. Running it on an older runtime prints an explicit
upgrade message and exits 1.

Claude-Session: https://claude.ai/code/session_01TGA6sFTUGsJ6oXhfpjKEnh
* footer requirement, and the commitlint scope rule
- CONTRIBUTING.md: replace the type-less commit example with a
  commitlint-valid conventional format (verified against commitlint);
  correct all four coverage mentions to the real jest thresholds
  (bulma-ui 99%, create-bestax 95%/78% branches); fix the npm package
  name (@allxsmith/bestax-bulma, plus create-bestax) and link VERSIONING.md
- CLAUDE.md: remove the stale-docs warning and asides now that the
  underlying docs are correct; point at VERSIONING.md again

Closes #206.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0131uD6QKmAij7Byk3SByyLh
* the @allxsmith/bestax-bulma/versions/bestax-bulma-prefixed.css
export is removed. Use versions/bestax-prefixed.css with classPrefix="bestax-".
* **bulma-ui:** React 16 and 17 are no longer supported; the minimum
supported React version is now 18.
* **bulma-ui:** Snackbar has been removed and merged into Toast; use Toast
with its positioning and queue props instead.
* **bulma-ui:** form controls now auto-wrap in Field/Control, and Checkbox
and Radio ship new themed visuals. See the 2.x -> 3.x migration guide.
* This version requires Node.js 18.0.0 or higher. The CLI now enforces this requirement and will exit with an error message if running on older Node.js versions. This aligns create-bestax with the bestax-bulma v2.x ecosystem.

* fix(create-bestax): correct Prettier formatting in index.ts
* None - all changes are additive and backward compatible
allxsmith added a commit that referenced this pull request Aug 29, 2026
Round 7. Both parsers accepted any dashed option and silently ignored it,
which in this script is not a usability wart but another silent-disable path —
the third found on this branch. Every optional flag here turns a safeguard OFF
by being absent, so a typo that is ignored exits 0 having disabled it:

  --tagg   a release leg resolves `latest` — the pin item 1 exists to add
           simply does not happen
  --exepct the installed-version assertion is skipped

Both now fail, naming the flags the mode does accept. A flag valid for the
other mode is rejected too, since `spec --dir` is as wrong as `spec --tagg`.

Fixed in check-consumer-sbom.mjs in the same pass, though Copilot only named
the sibling. No flag there is optional today, so a typo would currently be
caught by the required check — but that is a property of today's flag list
rather than a guarantee, and the first optional flag added would reopen it
silently. Round 5 is the precedent for sweeping the sibling rather than
waiting for it to be reported separately.

Deep review's three advisories need no change; all are documented trades. Its
#2 is worth a maintainer's explicit nod rather than a diff read: this job went
from running no repository code to executing event-ref code, which is the one
genuinely new trust surface in this PR.
allxsmith added a commit that referenced this pull request Aug 29, 2026
* ci: pin, guard and de-duplicate the consumer-closure SBOM

Four of the five follow-ups #529 deferred (#530). Item 3 is deliberately
partial; see below.

1. Pin the released package to its release tag. `release.tag_name` is
   `<pkg>@X.Y.Z`, so the one package a release names is now installed at
   that exact version instead of resolving `latest`. That closes two real
   failures: the event fires immediately after `npm publish`, so a
   CDN-cached packument could stamp release 5.12.0 with an SBOM describing
   5.11.1; and a re-run days later resolved a newer `latest` and produced a
   filename `--clobber` cannot replace, leaving the release carrying two
   contradictory documents for the same package. The other three legs stay
   on `latest` — a release says nothing about them — which is why the
   decision is a script matching the tag to its matrix leg rather than an
   expression. The pin also buys a free assertion: `--expect` fails the job
   if the tree does not carry the version that was asked for.

   Pinning is retried, because it trades "resolves the wrong version" for
   "may not resolve at all" while the packument propagates. Without that,
   the released leg — the one the release cares about — would be the one
   most likely to ship no SBOM.

2. Assert the document is still a consumer closure. Nothing checked, so a
   syft, cataloger or exporter change shipped silently green. Deliberately
   not a count: 48c57d5 reverted exactly that, because live registry
   closures drift on somebody else's release and a false-red generator is
   the cry-wolf failure #391 and #525 already fixed. Instead every
   catalogued package must resolve to registry.npmjs.org, plus a floor.
   That catches inflation by its cause rather than its size, and replays
   both #529 regressions in the test sibling.

3. Move the install/stamp shell into scripts/ with node --test siblings
   (rule 9). The semver validation guards a value out of a published
   tarball that flows into $GITHUB_OUTPUT and syft's config heredoc, and
   there was no way to test it where it lived. This job now needs a
   checkout; it takes the event ref rather than `main`, because it asserts
   a property of a document it just generated rather than a third party's
   claim, and because `main` would make the only available verification —
   a dispatch on a branch — impossible.

   NOT done: extracting `attach-sbom`'s upload branching. That job has no
   checkout and runs no repository code, which is the whole reason it is
   safe to give it `contents: write`. Adding one to gain a unit test is a
   worse trade than the test is worth. #530 stays open for it.

4. Build the artifact name once. Four occurrences collapse to one step
   output. `sign-sbom` and `attach-sbom` glob these names from their own
   jobs and cannot read a step output across a job boundary, so this is
   four to one, not eight — what keeps those globs honest is that the
   prefix is now one exported constant pinned by a test.

5. Scan the lockfile rather than the tree. syft walked every file under
   node_modules — thousands per leg, twice — to satisfy a cataloger that
   only reads the root lockfile. The node_modules exclusion stays and is
   now structurally unnecessary rather than merely correct.

None of this is exercised by a PR: the job runs on release, schedule and
workflow_dispatch only. Verified by dispatch and by reading the generated
documents, which is how every defect in #529 was found.

* ci: pass the lockfile as sbom-action's file input, not its path input

sbom-action prefixes `path` with syft's `dir:` scheme, so the lockfile
pointed at it failed with `not a directory source` on every matrix leg.
`file` is the input that takes one. Found by dispatch (33260609524), which
is the only way this job is exercised at all.

* ci: re-measure the consumer closure sizes from a real run

bestax-mcp read 94 against 95 entries, which two structural entries can
never produce. The measured closure is 93. Numbers now cite the run they
came from and say plainly that they drift, so the next reader re-measures
instead of trusting them.

* ci: read both SBOM formats, and stop the scan source leaking a runner path

Addresses three review findings on #594, one of which turned out to be a
live defect this branch introduced.

The scan source is a lockfile-only DIRECTORY, not the lockfile as a file
source. Passing it as sbom-action's `file` input worked and did remove the
node_modules walk, but a file source makes syft emit the scanned file as a
component: every CycloneDX document came out carrying
`/home/runner/work/_temp/consumer/package-lock.json`. That is the runner-path
leak #529 fixed for SPDX, reintroduced in the other format — and it was
signed and attached. Copying the lockfile into an otherwise empty directory
keeps the document shape #529 measured while still removing the walk.

The guard now reads BOTH documents. An SPDX-only guard is what passed the
leak above: the two come from separate sbom-action invocations, every
.spdx.json was clean, and nothing looked at the .cdx.json that was not.
CycloneDX states the same claims in its own vocabulary, so `pkg:npm/` purls
stand in for registry.npmjs.org download locations and metadata.component
carries the subject.

The guard also asserts the target package is in its own closure at the
stamped version. Without it, a wrong install spec or a cataloger dropping the
direct dependency produces a well-formed closure OF SOMETHING ELSE and passes
every other assertion.

Missing required flags now exit 2 rather than 1. The header promised the
codes stay distinct so a mistyped invocation is not reported as a
supply-chain failure, and the checks were on the assertion path; parseArgs
owns them now. The test asserting the old behaviour asserted it against a
comment that said the opposite.

* ci: stop syft cataloging the scan file, and correct the leak's provenance

The runner-path leak in the CycloneDX documents is NOT something this branch
introduced, and the previous commit message said it was. Run 32706731377 — a
scheduled run on main from before this branch existed — carries
`/home/runner/work/_temp/consumer/package-lock.json` in all four .cdx.json
files. It has been shipping in every release since #529, which fixed the same
leak for SPDX and never looked at the other format. Switching to a file
source moved which path was leaked; it did not create the leak.

The cause is that file metadata is cataloged independently of
`default-catalogers`, so restricting that to the lock cataloger never touched
it. `file.metadata.selection: none` is what turns it off. The exporters
disagreed about naming, which is why only one format showed it: SPDX writes a
relative `package-lock.json` into its `files` array, CycloneDX writes a
`type: file` component with the absolute path.

Also drops backticks from the syft heredoc. It is unquoted — it interpolates
$PACKAGE and $VERSION — so the existing "`files:`" in a comment was being run
as a command and substituted away, leaving "publishing without  field" in the
generated config. Inert inside a YAML comment; not inert in general.

* ci: make the closure guard a gate rather than a detector

Deep review finding #1 on #594, and it was right. sbom-action generates and
uploads in one step, so the artifact existed before anything had inspected it
— and `sign-sbom`/`attach-sbom` deliberately do not require `consumer-sbom` to
have succeeded. On a real release that meant a document the guard had already
rejected still got signed and permanently attached to the release. The run
went red and the bad SBOM shipped anyway, which is most of the value of the
guard gone.

`upload-artifact: false` on both generators plus one explicit upload after the
assertion closes it: nothing is published unless both documents check out.

This preserves the degradation #529 designed for rather than trading it away.
A leg that fails for any reason now produces no artifact at all, the globs in
the downstream jobs expand to nothing, and `attach-sbom` emits its ::warning::
while the repository SBOMs still ship. The alternative — gating those jobs on
`needs.consumer-sbom.result` — would have been worse: the matrix is
fail-fast: false precisely so one bad leg cannot take the other three with it,
and a job-level gate would have done exactly that.

Also makes the non-registry message say what to do when the entry is a
legitimate git/tarball/alias runtime dependency rather than a leak (finding
#3), so the next person to hit it decides about the dependency instead of
reflexively widening the check.

* ci: reject an absolute path in SPDX's files array too

The origin loop reads packages and components; SPDX keeps file entries in a
separate `files` array that it never saw. syft writes those relative today —
a bare `package-lock.json`, which leaks nothing and still passes — but that
array is exactly where the absolute path would land if the file config
changed, and an absolute path there is the SPDX shape of the leak that shipped
in every .cdx.json from #529 until this branch. Checked rather than trusted to
stay relative.

* ci: anchor the semver check at both ends

Copilot was right: the shape test was `/^\d+\.\d+\.\d+/`, a PREFIX match, so
`1.2.3garbage`, `1.2.3.4` and `01.2.3` all passed a function whose error
message says "is not a semver version". A validator that accepts what it
claims to reject is the failure .github/CLAUDE.md's checklist ends on.

Replaced with semver.org's anchored grammar, which still admits prerelease and
build metadata — a regex that reds a real release would be worse than the
loose one it replaces.

The two checks are now ordered character-test-first, and that order is
load-bearing rather than incidental. The anchored grammar rejects everything
the character test rejects, so running it first would leave the character test
unreachable — dead code wearing the label of the control that matters most
here. Running the character test first keeps it live and makes each error name
the real problem: a value carrying a newline is reported as an injection
attempt rather than as a formatting quibble.

Tests assert which check catches what, rather than only that something threw.
Rejected-for-the-wrong-reason is how the prefix bug survived the first review.

* ci: validate the SPDX subject too, and stop overstating the syft run

Two more Copilot findings, both right.

The SPDX subject was exempted by name and never validated, while the
CycloneDX one was checked. So an SPDX document could name the wrong version —
or omit the claim entirely — while every dependency entry in it was correct,
and the guard would pass it. The subject is the document's identity, not a
structural detail to skip past, so both formats are now checked the same way
and the tests assert the symmetry rather than one side of it.

The normalize() comment claimed both documents come from "the same syft run".
They do not: they are two separate sbom-action invocations sharing a scan
directory and a config. That distinction is the entire reason each document is
inspected independently — the leak that shipped from #529 until this branch
was in every CycloneDX document and in none of the SPDX ones, which cannot
happen if they are two renderings of one result. A comment that overstates its
mechanism is worse than no comment, because the next reader stops checking.

* ci: cross-check the two documents, and stop calling a purl provenance

Copilot again, and the finding lands on something I wrote rather than
inherited: the guard claimed CycloneDX asserts registry provenance "with a
pkg:npm/ purl". It does not. syft's lock cataloger builds
pkg:npm/name@version from the name and version alone and records `resolved`
nowhere in a CycloneDX document — not in externalReferences, not in
properties. Verified against a real component from run 33262407242. So that
test is an ECOSYSTEM test: it catches a github-actions entry or a bare file
component, which is the leak class it was added for, but a git, tarball,
private-registry or aliased dependency passes it untouched.

Overstating it was the more serious half. A comment that claims a control it
does not have is worse than no comment, because the next reader stops
checking — and this is the second time on this branch I have written one.

The gap itself is closed by a fourth assertion: the two documents must list
the same name@version set. Nothing made them agree by construction — they are
separate syft runs, which is exactly how the #529 leak lived in every
CycloneDX document and no SPDX one — so this is a real check, and it carries
the registry claim across: anything the weak purl test would admit has to
appear in SPDX too, where downloadLocation and the strong test are waiting.

That means both documents in one invocation. Two separate ones could each pass
while disagreeing with each other, which is the whole case being closed.

* ci: enforce that each file's contents match the flag it was passed as

The --spdx and --cdx flags were a claim about each file; normalize() detects
the format from the document's own shape. Nothing compared the two, which made
the flags decoration: hand the checker the CycloneDX file twice and every
per-document assertion passes, the same-closure assertion trivially agrees
with itself, and the release ships an asset named `.spdx.json` that is not
SPDX. A `format:` typo on either sbom-action step produces precisely that, and
it is the kind of defect the rest of this guard exists to catch.

Detected format is now compared against the flag before any other assertion
runs. Tests cover the same file passed twice, in both formats, and the swapped
pair — each individually well-formed, all three rejected.

* ci: compare the two closures as multisets, and warn on duplicates

Deep review advisory #1: crossCheck used Array.includes, which asks only
whether an identity appears at all. A package listed twice in one document and
once in the other therefore looked identical to both listing it once, so
asymmetric duplicate inflation passed silently. Counting is the same work and
answers the question the function claims to answer.

Duplicates within a single document are a WARNING rather than a failure, and
the split is deliberate. The doubled-catalogers regression (#529) reds today
only because the second copy carried no registry origin; a future duplication
that kept a valid origin in both documents would pass everything here, since
growth is explicitly allowed. But npm can legitimately place the same
name@version at two paths when it cannot hoist, and failing on that would red
somebody else's release for a tree shape nobody chose — the false-red
generator 48c57d5 reverted and #391/#525 are about.

A warning names the count and the offenders, which is enough for a human
reading a dispatch to recognise "every package is listed twice" at a glance,
and cannot cost a release. Measured before choosing: zero duplicates across
all 208 catalogued entries in the four closures of run 33263381732.

* ci: detect formats by their own markers, and stop a multi-version false red

Round 3 of review, four findings, all real.

The target check took the FIRST entry matching the package name. npm can carry
more than one version of a package in a closure and nothing orders the
document by depth, so a nested older copy emitted ahead of the direct one
reported a mismatch while the stamped version sat further down the list. That
is a false red on a good release — the failure this whole guard is written to
avoid. Any matching entry now satisfies it, and the message lists every
version actually found rather than just the first.

normalize() keyed on array shape alone, so any object with a `packages` array
read as SPDX. An exporter result that had lost `spdxVersion` is no longer a
valid SPDX document and is not what the asset's name promises, yet it
satisfied the --spdx/--cdx role check and would have shipped. Detection now
requires each format's own top-level marker as well as its array, and the
fixtures carry the markers a real document has.

The SPDX files-array check rejected only ABSOLUTE names. `work/_temp/scan/…`
discloses the same layout as `/home/runner/work/_temp/scan/…`, so it now
rejects any name carrying a path separator; the scan directory holds one file,
so the only legitimate entry is a bare `package-lock.json` (`./` prefix
tolerated).

Recorded what this guard does NOT defend, per the deep review: the
registry-origin assertion is the second line against inflation, not the first.
A genuine npm package resolving from registry.npmjs.org and present in both
documents passes every assertion here. What prevents that class is the scan
shape — a directory holding only the root lockfile — so weakening the scan
back to the installed tree on the grounds that the guard will catch it would
be wrong. It would not.

* ci: stop the rejection message re-introducing what it rejected

Copilot, round 4, and it is the sharpest finding on this PR. assertVersion
blocks a version carrying a newline from reaching $GITHUB_OUTPUT — and then
interpolated that same value verbatim into its own `::error::` complaint. A
GitHub Actions workflow command is terminated by a newline, so the message

  ::error::version contains unexpected characters: "1.0.0
  ::error::FORGED COMMAND"

emits a second, attacker-authored workflow command. The guard was performing
the attack in the course of reporting it. Reproduced before fixing.

Both scripts now render untrusted values through a `forLog` helper —
JSON.stringify, which escapes newlines, carriage returns, quotes and control
characters so the value can only ever be one inert line. Applied to every
value that came out of a published tarball, a generated document, or a release
tag: version strings, JSON parser messages, SBOM entry names and versions,
download locations, purls, file names, subject names, duplicate identities.

The same class was live in check-consumer-sbom.mjs, which reports entry names
straight out of a document whose contents are tarball-derived. Fixed there in
the same pass rather than waiting for it to be found separately.

Tests assert the property rather than the wording: no message may contain a
raw newline or a line beginning `::`.

* ci: sweep the remaining log-injection sites, and require --event

Round 5. My round-4 commit claimed forLog was applied to every value out of a
tarball or a generated document, and specifically that check-consumer-sbom.mjs
had been swept. That was wrong — Copilot found two live sites in that file and
both reproduce:

  readDocument: SyntaxError.message embeds a snippet of the offending source,
  raw newlines included, and it is printed under ::error::. Identical to the
  defect fixed in the sibling script one commit earlier.

  crossCheck: excess() builds `name@version` strings out of document values and
  main prints each discrepancy, so a CycloneDX-only entry named
  `evil\n::error::FORGED` reached the log intact.

Both now go through forLog. More usefully, the coverage is a SWEEP rather than
another list: a test poisons every string-valued field a document can carry —
names, versions, downloadLocations, purls, metadata.component, files — drives
both entry points and all three crossCheck orderings, and asserts no resulting
message contains a raw newline or carriage return. Eyeballing the call sites is
what missed these two after a pass that declared the file clean, so the test
now does the enumerating.

Separately: `spec` now requires --event. installSpec treats an absent event as
"not a release", so a malformed invocation exited 0 and resolved `latest` —
silently turning OFF the release pin this script exists to apply. A workflow
edit dropping the flag would have disabled item 1 with nothing reporting it.
--tag stays optional; schedule and dispatch legitimately have none.

* ci: count distinct names for the floor, and require an entry to identify itself

Round 6, two Copilot findings, both real.

The floor counted ENTRIES. Duplicates are only warned about — npm can
legitimately place a version at two paths — so three copies of the target
package cleared a floor of three while the closure had in fact collapsed to a
single package. Worse, symmetric copies also satisfy origin, target, subject
and the multiset cross-check, so nothing else would have caught it. Distinct
names is what "the document collapsed" actually means. Measured before
tightening: the smallest real closure carries 5 distinct names, so the floor of
3 keeps its headroom.

A catalogued entry must now carry a non-empty name and version. Without that,
an entry with a plausible origin but no identity produced no problem at all,
and identities() rendered a missing version as `?` — so when both syft runs
omitted the same metadata the floor and the cross-check agreed with each other
and a malformed document shipped. Measured before requiring it: 428 entries
across the four closures, none missing either field.

Deep review's three advisories need no change: the origin check's false-red
exposure, the release-only paths being unexercisable, and the three unpinned
legs are all trades already argued in the PR body and deliberately kept.

* ci: reject unknown flags instead of ignoring them

Round 7. Both parsers accepted any dashed option and silently ignored it,
which in this script is not a usability wart but another silent-disable path —
the third found on this branch. Every optional flag here turns a safeguard OFF
by being absent, so a typo that is ignored exits 0 having disabled it:

  --tagg   a release leg resolves `latest` — the pin item 1 exists to add
           simply does not happen
  --exepct the installed-version assertion is skipped

Both now fail, naming the flags the mode does accept. A flag valid for the
other mode is rejected too, since `spec --dir` is as wrong as `spec --tagg`.

Fixed in check-consumer-sbom.mjs in the same pass, though Copilot only named
the sibling. No flag there is optional today, so a typo would currently be
caught by the required check — but that is a property of today's flag list
rather than a guarantee, and the first optional flag added would reopen it
silently. Round 5 is the precedent for sweeping the sibling rather than
waiting for it to be reported separately.

Deep review's three advisories need no change; all are documented trades. Its
#2 is worth a maintainer's explicit nod rather than a diff read: this job went
from running no repository code to executing event-ref code, which is the one
genuinely new trust surface in this PR.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant