Skip to content
Open
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
6 changes: 3 additions & 3 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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/
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/cla.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
16 changes: 12 additions & 4 deletions .github/workflows/license-check.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
7 changes: 6 additions & 1 deletion .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
73 changes: 73 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -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-<name>.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/<id>/` 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`.
11 changes: 4 additions & 7 deletions index.html
Original file line number Diff line number Diff line change
Expand Up @@ -31,13 +31,10 @@
html { background: #e7e6e1; }
html[data-theme='dark'] { background: #1b1d20; }
</style>
<script>
try {
var t = localStorage.getItem('scanruler.uitheme')
if (t === 'dark' || (t !== 'light' && matchMedia('(prefers-color-scheme: dark)').matches))
document.documentElement.dataset.theme = 'dark'
} catch (e) {}
</script>
<!-- A file, not an inline script, so the Content-Security-Policy in
public/_headers needs no hash or 'unsafe-inline' for it. Classic and
blocking on purpose: it must run before the first paint. -->
<script src="/theme.js"></script>
</head>
<body>
<div id="root"></div>
Expand Down
28 changes: 28 additions & 0 deletions public/_headers
Original file line number Diff line number Diff line change
@@ -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 <style> in index.html.
# img-src data: and blob: for textures, thumbnails and exports.
# connect-src https: for the file store (VITE_FILES_URL) and any ?open= link;
# 127.0.0.1 / localhost on any port for the agent link (mcp/).
/*
Content-Security-Policy: default-src 'self'; script-src 'self' 'wasm-unsafe-eval'; worker-src 'self' blob:; style-src 'self' 'unsafe-inline'; img-src 'self' data: blob:; font-src 'self' data:; connect-src 'self' https: data: blob: http://127.0.0.1:* ws://127.0.0.1:* http://localhost:* ws://localhost:*; media-src 'self' blob:; object-src 'none'; base-uri 'self'; form-action 'self'; frame-ancestors 'none'
X-Content-Type-Options: nosniff
Referrer-Policy: strict-origin-when-cross-origin
X-Frame-Options: DENY

# The Reverse engineer's CAD kernel (occt-wasm, an Emscripten build) generates
# its bindings with new Function, so only the two worker scripts that load it
# get 'unsafe-eval'. A worker takes its policy from its own response; the page
# keeps the strict one. `!` drops the policy from the rule above first, since
# two values for one header would be joined into a second, stricter policy.
/assets/reWorker-*
! Content-Security-Policy
Content-Security-Policy: default-src 'self'; script-src 'self' 'wasm-unsafe-eval' 'unsafe-eval'; connect-src 'self' data: blob:; object-src 'none'; base-uri 'self'
/assets/reHelperWorker-*
! Content-Security-Policy
Content-Security-Policy: default-src 'self'; script-src 'self' 'wasm-unsafe-eval' 'unsafe-eval'; connect-src 'self' data: blob:; object-src 'none'; base-uri 'self'
8 changes: 8 additions & 0 deletions public/theme.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
// SPDX-License-Identifier: AGPL-3.0-only
// Sets the dark theme before the first paint, from the same key the settings
// store keeps it under, so a dark chassis never flashes light on load.
try {
var t = localStorage.getItem('scanruler.uitheme')
if (t === 'dark' || (t !== 'light' && matchMedia('(prefers-color-scheme: dark)').matches))
document.documentElement.dataset.theme = 'dark'
} catch (e) {}
13 changes: 13 additions & 0 deletions vite.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading