Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ Run the full set before declaring a change done. CI runs the same commands plus
- Squash commit messages use conventional prefixes scoped to the crate: `feat(codecs): mozjpeg-rs encoder`, `fix(cli): exit code on partial batch failure`. Intermediate commits on a branch do not matter.
- A behaviour change updates `CHANGELOG.md` under Unreleased in the same PR. The file follows the conventional-changelog convention: `### ⚠️ BREAKING CHANGES` first when a release breaks the library API or the CLI, then `### Features` / `### Bug Fixes` / `### Performance Improvements`, entries as `* **scope:** subject` with the same crate scope as the commit. A breaking entry says what to do instead. The `⚠️` heading (U+26A0 U+FE0F, the colour emoji form; pasting often drops the U+FE0F) is the one exception to the emoji and sentence-case rules below.
- Architecture changes get a new ADR in `docs/adr`, not a rewrite of an existing one. A new ADR also gets a row in `docs/adr/README.md`, an entry in `.greptile/files.json` scoped to the code it governs, and an update to any `.greptile/config.json` rule it changes, in the same PR.
- Releases: push a `v*` tag on the tip of `main`, and merge nothing that touches `.github/workflows` until the release run finishes: GitHub refuses to create the release from `GITHUB_TOKEN` when `main` has workflow changes the tagged commit lacks (ADR-0006 item 5). `dist-workspace.toml` drives `.github/workflows/release.yml`; edit the TOML and run `dist generate`, never the workflow by hand. The release `plan` job fails a PR when the two disagree. The same tag stages `crates/sqzer-wasm` on npm as `sqzer`: `release.yml` calls `.github/workflows/publish-npm.yml`, which runs `crates/sqzer-wasm/pack.sh` and runs `npm stage publish` through npm's trusted publishing, no token stored (ADR-0011 D5). The version is not live until it is approved under Staged Packages on npmjs.com, with 2FA. The last step of a release comes after that approval: bump the version `sqzer-dev/sqzer.dev` imports, in its `site/package.json`, or merge the Dependabot pull request that does (ADR-0011 D6). This repository's release workflow does not touch the page. The trusted publisher on npmjs.com names `release.yml`, the calling workflow, and allows staged publishing only.
- Releases: push a `v*` tag on the tip of `main`, and merge nothing that touches `.github/workflows` until the release run finishes: GitHub refuses to create the release from `GITHUB_TOKEN` when `main` has workflow changes the tagged commit lacks (ADR-0006 item 5). `dist-workspace.toml` drives `.github/workflows/release.yml`; edit the TOML and run `dist generate`, never the workflow by hand. The release `plan` job fails a PR when the two disagree. The same tag stages `crates/sqzer-wasm` on npm as `sqzer`: `release.yml` calls `.github/workflows/publish-npm.yml`, which runs `crates/sqzer-wasm/pack.sh` and runs `npm stage publish` through npm's trusted publishing, no token stored (ADR-0011 D5). The version is not live until it is approved under Staged Packages on npmjs.com, with 2FA. The last step of a release comes after that approval: bump the version `sqzer-dev/sqzer.dev` depends on, in its `package.json` and lockfile, or merge the Dependabot pull request that does (ADR-0011 D6). This repository's release workflow does not touch the page. The trusted publisher on npmjs.com names `release.yml`, the calling workflow, and allows staged publishing only.
- New dependencies: state the licence in the PR description and check it is on the `deny.toml` allow-list. Prefer crates that already speak `imgref` or implement `image`'s traits, since adapters then come cheap.

## Testing expectations
Expand Down
2 changes: 1 addition & 1 deletion docs/adr/0011-browser-build.md
Original file line number Diff line number Diff line change
Expand Up @@ -202,7 +202,7 @@ That repository's Pages workflow deploys on every merge to its `main`, so a page
2. [x] `oxipng` on wasm32 with `freestanding`, the `png` encoder removed, the golden PNG test running on wasm32 too; `.cargo/config.toml`. (Done on 2026-10-01, the golden test on 2026-10-02: `oxipng` with `freestanding` on wasm32, `PngEncoder` removed from `sqzer-codecs`, `simd128` in `.cargo/config.toml`, and the D1 wording of item 6 in README, `CONTRIBUTING.md`, `CLAUDE.md` and `ROADMAP.md`. The golden test came with item 3's harness: `crates/sqzer-wasm/src/tests.rs` runs the lossless round trip to a score of 100, PNG included, and the portable golden scores on wasm32. By hand, the wasm32 build's PNG output is byte for byte the native build's for nine inputs, a 2 megapixel image at effort 0, the default and effort 10 among them. Two AVIF inputs differ, before the encoder: the planes `rav1d` returns are identical, and the `yuv` crate's YUV to RGB conversion rounds differently in its x86 SIMD path than in its scalar and wasm paths, by up to 2 in 255, and 7 in 255 after the ICC conversion of the Display P3 fixture. That is independent of this change and of `simd128`, and it means D1's "output identical on every target" holds for the encoders and not for the pixels an AVIF input decodes to.)
3. [x] The `sqzer-wasm` surface of D3 with `wasm-pack test --node` over the fixtures: every raster format in and out, an `EncoderUnavailable` for JPEG XL with `availableIn`, `onTrial` called once per trial, `maxPixels` refusing a 25 megapixel header, `fromPixels` round-tripping a fixture's pixels. `decodeAny` cannot run in Node; it is checked in Chrome, Firefox and Safari with item 5, including `createImageBitmap` on an SVG blob inside a Worker, which may need the main thread in some of them. (Done on 2026-10-02; the browser check of `decodeAny` goes with item 5, as the item says: the D3 surface in `crates/sqzer-wasm`, and 14 tests under `wasm-pack test --node`, among them every raster fixture decoded, the AVIF ones included, and the portable golden scores on wasm32, which closes what items 1 and 2 left for this one. `decodeAny` is tested as far as Node goes: its own decoder first, the error without a canvas, and the size, limit and pixel plumbing against a fake `createImageBitmap` and `OffscreenCanvas`; the real ones wait for item 5. Where the code differs from the text above: the types come from `tsify`, which `tsify-next` was folded back into, through its `Ts<T>` wrapper; `wasm-bindgen-futures` joins the dependencies, since an `async` export compiles to it; the class is `SqzerImage`, because `Image` is the DOM's; `scale` is a factor, `0.5`, not `"50%"`; `fromPixels` takes `{ maxPixels }` as a fourth argument, because the pixel limit holds for it as for `decode`; the package registers a sniffer for SVG, so `decode` says `DecoderUnavailable` for one and `decodeAny` falls back on that kind alone; `serde-wasm-bindgen` never sees a key it does not know, so a misspelt option is refused by a check of the object's keys; `sqzer` gains one feature per portable codec for the package to select from; and the crate stays `cdylib` and `rlib`, not D4's `cdylib` only, because a static musl target has no `cdylib` and `cargo build --workspace` fails there on a crate with nothing else. What the work found: the workflow-level `RUSTFLAGS` of `ci.yml` replaced the flags of `.cargo/config.toml`, so the wasm job had been building without `simd128` since item 2, and the job now unsets it; an unoptimised build overflows the linker's 1 MiB stack while decoding a fixture, a trap with no message, so wasm32 builds link with 8 MiB; `wasm-pack` 0.15 does run `wasm-opt -O` on a release build, with a binaryen that rejects SIMD and bulk memory, so D4's "off" is `wasm-opt = false` in the manifest and item 7 needs the feature flags passed. The release package is 8.1 MB, 1.77 MB over brotli, not the 1.4 MB of D3: the table of section 1 predates the AVIF decoder and `oxipng` on wasm32. For item 4: `wasm-pack` names the package `sqzer-wasm` after the crate, leaves `snippets/`, where `decodeAny`'s JavaScript lives, out of the `files` of `package.json`, so `npm publish` would ship a module that imports a file it lacks, and finds no `LICENSE` file in the crate directory. Two things in the module are not ours: `v_frame` exports a `ChromaSampling` enum to JavaScript, and `sqzer-rav1d` keeps its twenty `dav1d_*` C entry points as wasm exports.)
4. [x] `publish-npm.yml`, the `dist` config and `dist generate`; the trusted publisher on npmjs.com; a rehearsal with `pr-run-mode = "upload"` on a branch, then the first publish with the 0.3.0 tag. (Done on 2026-10-02: `publish-npm.yml`, called from `release.yml` through `publish-jobs = ["homebrew", "./publish-npm"]`, with `github-custom-job-permissions` giving it `id-token` and `contents`. The build is `crates/sqzer-wasm/pack.sh`, which also repairs what item 3 found in `wasm-pack`'s output: the name becomes `sqzer`, and `snippets` and the two licence texts join `files`. It then installs the tarball in an empty project and runs it from there; with `snippets` taken out of `files` again that run fails with `ERR_MODULE_NOT_FOUND` for `canvas.js`. Where it differs from D5: the trusted publisher names `release.yml`, not `publish-npm.yml`, because for a workflow run through `workflow_call` npm checks the name of the calling workflow, and the job runs `npm stage publish`, not `npm publish`: the trusted publisher is left at its default, staged publishing only, so a pushed tag alone puts nothing on npm and the version goes live when it is approved on npmjs.com with 2FA, which needs npm 11.15 on the runner; `--provenance` is redundant under trusted publishing from a public repository and stays as a statement. The rehearsal is not `pr-run-mode = "upload"`: on a pull request `host` is skipped and every publish job needs it, so that mode builds six archives and never reaches this workflow. The wasm job of `ci.yml` runs `pack.sh` on every pull request instead, and `npm stage publish --dry-run` of the tarball passes. What no rehearsal covered was the OIDC exchange with npm and the upload to the GitHub release; both ran for the first time with the `v0.3.0` tag, and passed. The tarball is attached to the release before it is staged, so if npm refuses it the file is there to publish by hand, and a run repeated after the version is live skips the staging. The trusted publisher was set up the same day, `sqzer@0.3.0` was staged by the release run, approved, and is on npm with a provenance attestation in place of the 0.0.1 placeholder; installed from the registry it passes `tests/package.mjs`.)
5. [x] `github.com/sqzer-dev/sqzer.dev`: the page, its Pages workflow, `CNAME`, Dependabot on `sqzer`, DNS for `sqzer.dev`; this README pointing at the page and the version bump in the Releases bullet of `CLAUDE.md`. (Done on 2026-10-02: `github.com/sqzer-dev/sqzer.dev`, public, MIT or Apache-2.0, the page under `site/`, deployed by its Pages workflow on every push to its `main`, Dependabot on `site/package.json`, and live at `https://sqzer.dev` the same day: the apex on the four Pages A records, `www` a CNAME to `sqzer-dev.github.io`, HTTPS enforced, the MX and TXT records of the domain's mail left as they were. Where it differs from D6: there is no `CNAME` file, because a site deployed from Actions takes its domain from the Pages settings and ignores the file; the version is written once, in `site/package.json`, and `worker.js` reads it at run time to build the jsDelivr URL, so a Dependabot pull request is the whole bump and there is still no build step; the page carries a Content-Security-Policy that allows its own origin and jsDelivr and nothing else. Its look is a placeholder: the design is later work in that repository. The browser check item 3 left to this one ran in Chrome 139 and Firefox 156 on Windows and Safari on iOS 18.7, through the page's `/selftest/`: `decodeAny` reads an SVG on the page, sharp at an asked width, and refuses one in a worker in all three, where `createImageBitmap` will not take an SVG blob, so the page draws an SVG itself and hands the pixels to `fromPixels`; Safari decodes HEIC through the canvas on the page and in a worker, and a refused image closes its bitmap; the package's defaults and a portable build of the CLI give the same bytes for `pattern-rgb.jpg`. `crates/sqzer-wasm/README.md` says so for SVG; the doc comment of `decode_any` still says "SVG everywhere" and is a follow-up, since it ships in `sqzer.d.ts`.)
5. [x] `github.com/sqzer-dev/sqzer.dev`: the page, its Pages workflow, `CNAME`, Dependabot on `sqzer`, DNS for `sqzer.dev`; this README pointing at the page and the version bump in the Releases bullet of `CLAUDE.md`. (Done on 2026-10-02: `github.com/sqzer-dev/sqzer.dev`, public, MIT or Apache-2.0, the page under `site/`, deployed by its Pages workflow on every push to its `main`, Dependabot on `site/package.json`, and live at `https://sqzer.dev` the same day: the apex on the four Pages A records, `www` a CNAME to `sqzer-dev.github.io`, HTTPS enforced, the MX and TXT records of the domain's mail left as they were. Where it differs from D6: there is no `CNAME` file, because a site deployed from Actions takes its domain from the Pages settings and ignores the file; the version is written once, in `site/package.json`, and `worker.js` reads it at run time to build the jsDelivr URL, so a Dependabot pull request is the whole bump and there is still no build step; the page carries a Content-Security-Policy that allows its own origin and jsDelivr and nothing else. Its look is a placeholder: the design is later work in that repository. The browser check item 3 left to this one ran in Chrome 139 and Firefox 156 on Windows and Safari on iOS 18.7, through the page's `/selftest/`: `decodeAny` reads an SVG on the page, sharp at an asked width, and refuses one in a worker in all three, where `createImageBitmap` will not take an SVG blob, so the page draws an SVG itself and hands the pixels to `fromPixels`; Safari decodes HEIC through the canvas on the page and in a worker, and a refused image closes its bitmap; the package's defaults and a portable build of the CLI give the same bytes for `pattern-rgb.jpg`. `crates/sqzer-wasm/README.md` says so for SVG; the doc comment of `decode_any` still says "SVG everywhere" and is a follow-up, since it ships in `sqzer.d.ts`.) (Since 2026-10-03 the page is decided by its own records. ADR-0002 of `sqzer-dev/sqzer.dev` replaces two things D6 says about it: the page is a React and TypeScript application built by Vite, not plain HTML without a build step, and `sqzer` is a dependency in its root `package.json`, installed from npm and served from the page's own origin, not imported from jsDelivr. jsDelivr left its Content-Security-Policy, and `/selftest/` became a test suite in that repository. What D6 decided about the repository, the licence and the release procedure stands: https://github.com/sqzer-dev/sqzer.dev/blob/main/docs/adr/0002-stack.md)
6. [x] The wording changes of section 5 in README, `CONTRIBUTING.md`, `CLAUDE.md` and `ROADMAP.md`. (Done across the items: the D1 wording and the `clang` requirement with item 2, the AVIF and `oxipng` notes and the layout entry for `crates/sqzer-wasm` with items 1 to 3, the `CHANGELOG.md` entries with each item's pull request, the link to the page and the Releases step of `CLAUDE.md` with item 5. `ROADMAP.md` lost its section on the page; its note on the licence gaps moved to the section on the AGPL build, which no longer lists AVIF decoding among them.)
7. [x] Measure `wasm-opt -O` on the search and the AVIF encode; enable it in `[package.metadata.wasm-pack]` only if it is faster. (Measured on 2026-10-03, and it is not faster, so `wasm-opt` stays off. The release module of 0.3.0 against the same module through `wasm-opt -O`, once with binaryen 117, the version `wasm-pack` 0.15 downloads, and once with 133, the current one, each given the features `rustc` enables for the target: `--enable-simd --enable-bulk-memory --enable-nontrapping-float-to-int --enable-sign-ext --enable-mutable-globals --enable-reference-types --enable-multivalue`. The input is a 2048 x 1365 photograph of `clic2025/training`, decoded once; the search is the default one to JPEG, five trials on that image, and the AVIF encode is at quality 60. In Node 24.18, a fresh process per run under `hyperfine`: the search takes 5.82 s ± 0.68 s as built, 5.73 s ± 0.13 s with 117 and 5.77 s ± 0.12 s with 133; the AVIF encode 19.15 s ± 0.33 s, 19.67 s ± 0.21 s and 20.80 s ± 0.44 s, and run again in the opposite order the three land within 5 % of each other with no order among them. In Firefox 155, a fresh worker per run: the search takes 5.6 to 5.8 s as built, 6.0 to 6.1 s with 117 and 5.9 to 6.3 s with 133; the AVIF encode 20.1 to 22.0 s, 20.7 to 20.9 s and 20.3 to 21.4 s. The output bytes are the same from all three modules in both engines. The module shrinks from 8.08 MB to 7.60 MB and grows over brotli, from 1.81 MB to 1.84 MB, and binaryen 117 takes 79 s on 12 cores for it where 133 takes 8 s. Not measured: Safari, and Chrome itself, which Node's V8 stands in for.)

Expand Down
Loading