From 3ffdec0a14ea69d0d9638ab1512dfdf000da2db2 Mon Sep 17 00:00:00 2001 From: Jozkah <38704522+Jozkah@users.noreply.github.com> Date: Sun, 1 Nov 2026 00:57:45 +0000 Subject: [PATCH 1/4] ci: pin workflow actions and harden license and release jobs Actions are pinned to commit SHAs with the tag kept as a comment, so a moved tag cannot change what runs. The license check gets a read-only token, a concurrency group and a timeout, and pins license-checker to 25.0.1 so the gate is deterministic. Release runs are serialised. What each workflow runs is unchanged. --- .github/workflows/ci.yml | 6 +++--- .github/workflows/cla.yml | 2 +- .github/workflows/license-check.yml | 16 ++++++++++++---- .github/workflows/release.yml | 7 ++++++- 4 files changed, 22 insertions(+), 9 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f809657f..9123966f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -22,8 +22,8 @@ jobs: runs-on: ubuntu-24.04 timeout-minutes: 20 steps: - - uses: actions/checkout@v5 - - uses: actions/setup-node@v5 + - uses: actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09 # v5 + - uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5 with: node-version: 22 cache: npm @@ -40,7 +40,7 @@ jobs: run: npm run e2e:ci - name: Retain browser diagnostics if: ${{ failure() }} - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 with: name: browser-diagnostics path: e2e-out/ci/ diff --git a/.github/workflows/cla.yml b/.github/workflows/cla.yml index d9529c96..a0fd0564 100644 --- a/.github/workflows/cla.yml +++ b/.github/workflows/cla.yml @@ -36,7 +36,7 @@ jobs: runs-on: ubuntu-24.04 timeout-minutes: 5 steps: - - uses: contributor-assistant/github-action@v2.6.1 + - uses: contributor-assistant/github-action@ca4a40a7d1004f18d9960b404b97e5f30a505a08 # v2.6.1 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} with: diff --git a/.github/workflows/license-check.yml b/.github/workflows/license-check.yml index 124f00c1..9120f9a5 100644 --- a/.github/workflows/license-check.yml +++ b/.github/workflows/license-check.yml @@ -34,19 +34,27 @@ on: pull_request: workflow_dispatch: +permissions: + contents: read + +concurrency: + group: license-check-${{ github.ref }} + cancel-in-progress: true + jobs: npm-licenses: runs-on: ubuntu-24.04 + timeout-minutes: 15 steps: - - uses: actions/checkout@v5 - - uses: actions/setup-node@v5 + - uses: actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09 # v5 + - uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5 with: node-version: 22 cache: npm - run: npm ci - name: npm license allowlist run: >- - npx --yes license-checker --excludePrivatePackages + npx --yes license-checker@25.0.1 --excludePrivatePackages --excludePackages "meshstep@0.1.1" --onlyAllow "MIT;Apache-2.0;MIT OR Apache-2.0;BSD-2-Clause;BSD-3-Clause;ISC;0BSD;Zlib;CC0-1.0;MPL-2.0;Unicode-3.0;Unicode-DFS-2016;Unlicense;CC-BY-4.0;BlueOak-1.0.0" --summary @@ -55,7 +63,7 @@ jobs: - run: npm --prefix mcp ci - name: npm license allowlist (mcp/) run: >- - npx --yes license-checker --start mcp + npx --yes license-checker@25.0.1 --start mcp --excludePackages "scanruler-mcp@$(node -p "require('./mcp/package.json').version")" --onlyAllow "MIT;Apache-2.0;MIT OR Apache-2.0;BSD-2-Clause;BSD-3-Clause;ISC;0BSD;Zlib;CC0-1.0;MPL-2.0;Unicode-3.0;Unicode-DFS-2016;Unlicense;CC-BY-4.0;BlueOak-1.0.0" --summary diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index d9f1ce70..df28144a 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -21,13 +21,18 @@ on: permissions: contents: write +# Two runs never publish at once; a queued one waits rather than cancelling. +concurrency: + group: release + cancel-in-progress: false + jobs: release: if: ${{ github.repository == 'CNCKitchen/scanruler' }} runs-on: ubuntu-24.04 timeout-minutes: 10 steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09 # v5 with: fetch-depth: 0 - name: Publish the releases From 90b66a3733a9b25fff3313f9619c9566996b8889 Mon Sep 17 00:00:00 2001 From: Jozkah <38704522+Jozkah@users.noreply.github.com> Date: Sun, 1 Nov 2026 00:58:10 +0000 Subject: [PATCH 2/4] docs: add AGENTS.md for coding agents Summarises setup including the mcp package, the commands CI runs, the plugin boundary, display-units and license rules, and the style notes from CONTRIBUTING and the development guide. --- AGENTS.md | 73 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 73 insertions(+) create mode 100644 AGENTS.md diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 00000000..1c4b947d --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,73 @@ +# AGENTS.md + +Notes for coding agents working in this repository. The longer versions are +[CONTRIBUTING.md](CONTRIBUTING.md), [docs/development.md](docs/development.md) +and [plugins/README.md](plugins/README.md); when they disagree with this file, +they win. + +## What this is + +ScanRuler is a browser app (React 19, Vite 6, TypeScript, Three.js) for +measuring 3D scans against CAD. It deploys as static assets on Cloudflare +(`wrangler.jsonc`, output in `dist/`). Heavy work runs in module Web Workers, +some of it in WebAssembly (`manifold-3d`, `occt-wasm`). `mcp/` is a separate +Node package: the MCP server that lets an agent drive a running app. + +## Setup + +Node 22 or newer. + +```bash +npm ci +npm --prefix mcp ci +``` + +## Verify before you hand work back + +These are what CI runs (`.github/workflows/ci.yml`), in order: + +```bash +node scripts/check-plugin-boundary.mjs # nothing outside plugins/ imports from it +node scripts/check-units.mjs # on-screen lengths go through the display units +npm test # vitest, unit + acceptance +npm --prefix mcp test # the MCP server's own tests +npm run build # tsc --noEmit, then vite build +npm run e2e:ci # headless-Chrome checks; needs Chrome and a build +``` + +Acceptance tests that need the large real scans (`ballbar.stl`, the +`side bracket left.stl` / `block-marius.stl` pair and others) are not in the +repo and report as skipped without them. That is expected; do not treat the +skips as failures, and do not loosen a tolerance to make a test pass. + +`npm run dev` plus `node scripts/e2e-.mjs` runs a single browser check +against the dev server; see docs/development.md for which ones need no scan. + +## Rules the checks enforce + +- **Plugin boundary.** Workspaces such as Reverse engineer and Mesh Tools live + in `plugins//` and talk to the app only through + `src/plugins/api.ts`. Nothing in `src/` may import from `plugins/`; only + the registries (`src/plugins/registry.ts`, `src/core/workerPlugins.ts`) + name it, by glob. A build must stand with any plugin or none. +- **Display units.** Lengths shown to the user go through `src/core/units.ts` + (millimetres or inches). Do not write `mm` in UI text. A deliberate `mm` + (an id, a stored field name) carries a `units-ok` comment saying why. + The agent command API always answers in millimetres. +- **Dependency licenses.** Only permissive licenses (list in CONTRIBUTING.md + section 2 and `.github/workflows/license-check.yml`). Never GPL, LGPL, or + AGPL code we do not own; do not transcribe algorithms from such projects. + Ask before adding any dependency. + +## Style + +- New source files start with `// SPDX-License-Identifier: AGPL-3.0-only`. +- Numerical code belongs in `src/core/` or a plugin's `core/` and must run + without a browser: no DOM and no Three.js scene objects in fitting or + deviation maths. +- Comments explain why a non-obvious approach was chosen, not what a line does. + Match the surrounding code. +- User-visible changes get an entry at the top of `CHANGELOG.md`. +- The version lives only in `package.json`. When it changes, regenerate the + MCP command list: + `UPDATE_MCP_COMMANDS=1 npx vitest run tests/mcpCommands.test.ts`. From 9b8a6bb7672e26a074a68a7cdd67d1a4704e5038 Mon Sep 17 00:00:00 2001 From: Jozkah <38704522+Jozkah@users.noreply.github.com> Date: Sun, 1 Nov 2026 01:00:07 +0000 Subject: [PATCH 3/4] build: split three.js and react into their own chunks The main chunk drops from 3522 kB to 2769 kB minified; three and three-mesh-bvh (632 kB) and react, react-dom and zustand (195 kB) now load as separate chunks that stay cached across app releases. Total bytes on first load are unchanged. Lazy-loading workspaces was left out because it touches the plugin registry and app shell. --- vite.config.ts | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/vite.config.ts b/vite.config.ts index 717eccb6..30a462a3 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -17,6 +17,19 @@ export default defineConfig({ }, build: { chunkSizeWarningLimit: 1500, + // The libraries change far less often than the app, so each gets a chunk + // of its own that a browser keeps cached across releases, and the app's + // own chunk is the only large one a new version makes it fetch again. + rollupOptions: { + output: { + manualChunks(id) { + if (!id.includes('node_modules')) return undefined + if (/node_modules[\\/](three|three-mesh-bvh)[\\/]/.test(id)) return 'three' + if (/node_modules[\\/](react|react-dom|scheduler|zustand)[\\/]/.test(id)) return 'react' + return undefined + }, + }, + }, }, // The workers are module workers already (`type: 'module'` everywhere they // are made), and a worker may load a large package with a dynamic import From 827180f635efb48575b2c7615fb11e4ed12affa7 Mon Sep 17 00:00:00 2001 From: Jozkah <38704522+Jozkah@users.noreply.github.com> Date: Sun, 1 Nov 2026 01:06:38 +0000 Subject: [PATCH 4/4] feat: send a Content-Security-Policy and security headers public/_headers gives every static asset a CSP plus nosniff, a referrer policy and frame denial. The page allows its own scripts with wasm-unsafe-eval for the WebAssembly workers, https fetches for the file store and open links, and loopback http and WebSocket for the agent link. The CAD kernel workers alone also get unsafe-eval because the occt-wasm bindings are built with new Function. The theme bootstrap moves from an inline script to public/theme.js so the policy needs no inline-script allowance. Checked by serving the built dist with these headers and running the production smoke, agent and Reverse engineer kernel checks in headless Chrome with no CSP violations. --- index.html | 11 ++++------- public/_headers | 28 ++++++++++++++++++++++++++++ public/theme.js | 8 ++++++++ 3 files changed, 40 insertions(+), 7 deletions(-) create mode 100644 public/_headers create mode 100644 public/theme.js diff --git a/index.html b/index.html index c24a7858..cf7c4cde 100644 --- a/index.html +++ b/index.html @@ -31,13 +31,10 @@ html { background: #e7e6e1; } html[data-theme='dark'] { background: #1b1d20; } - + +
diff --git a/public/_headers b/public/_headers new file mode 100644 index 00000000..84e15850 --- /dev/null +++ b/public/_headers @@ -0,0 +1,28 @@ +# SPDX-License-Identifier: AGPL-3.0-only +# Response headers for every asset Cloudflare serves from dist/. +# +# Content-Security-Policy, by directive: +# script-src own bundle only; 'wasm-unsafe-eval' lets the workers compile +# manifold-3d and occt-wasm. Workers inherit it from their own +# response, which this same rule covers. +# style-src 'unsafe-inline' for the inline