From df03929ffa4886c7e1efaa9ce9aeeb0fac92689c Mon Sep 17 00:00:00 2001 From: Diederik Siderius Date: Wed, 12 Aug 2026 12:47:34 +0200 Subject: [PATCH] chore: add mise toolchain, agent docs, and CI hardening MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bring the project scaffolding up to standard so both humans and agents have a single, verifiable entry point. Toolchain (.mise.toml): - Pin Node to 22.20.0 — the exact version packaging/assemble.sh bundles into the .deb, so CI now tests the runtime that actually ships to the Pi. It was testing on Node 20. - Pin zizmor and actionlint so the workflow audits are runnable locally. - 13 tasks delegating to the existing npm scripts, plus `ci` and `ci-watch`. CI: - Route the gate through `mise run ci` so local and CI cannot drift. - Add least-privilege `permissions: contents: read`, a cancel-in-progress concurrency group, `workflow_dispatch`, and `persist-credentials: false`. - Add a zizmor job auditing the workflows on every run. - release.yml: set `persist-credentials: false`; nothing there pushes with git, so the credential need not survive into the docker build step. Coverage: - Wire @vitest/coverage-v8 with per-metric floors (50/48/40/50), scoped to src/plot and src/grbl — the framework-free core. src/ui and src/transport need a DOM and a live socket, so including them would only buy a floor low enough to be meaningless. Verified the floor actually fails the build. Agent + contributor docs: - AGENTS.md as the canonical agent config, CLAUDE.md as a symlink to it. It leads with the machine-safety rules — never interrupt a running plot (there is no resume), no limit switches, inverted Z, identity axis mapping, and the open-the-port-exactly-once constraint behind the CH340 wedge. - docs/agents/issue-tracker.md, and the four missing triage labels created on the remote. Fixes found on the way: - README had an empty code block where the rollback instructions belonged. - README claimed React 18; the project is on 19. - dependabot.yml had no cooldown, so a compromised release could be proposed the day it lands. Now 7 days on both ecosystems. zizmor reports no findings and actionlint is clean, with no suppressions added. Co-Authored-By: Claude Opus 5 (1M context) --- .github/dependabot.yml | 7 ++ .github/workflows/ci.yml | 61 ++++++++-- .github/workflows/release.yml | 5 + .gitignore | 1 + .mise.toml | 74 ++++++++++++ AGENTS.md | 120 ++++++++++++++++++ CLAUDE.md | 1 + README.md | 55 +++++++-- SECURITY.md | 2 +- docs/agents/issue-tracker.md | 63 ++++++++++ package-lock.json | 221 ++++++++++++++++++++++++++++++++++ package.json | 6 +- vite.config.ts | 19 +++ 13 files changed, 606 insertions(+), 29 deletions(-) create mode 100644 .mise.toml create mode 100644 AGENTS.md create mode 120000 CLAUDE.md create mode 100644 docs/agents/issue-tracker.md diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 6e1e7ac..96e9b29 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -6,6 +6,11 @@ updates: schedule: interval: weekly open-pull-requests-limit: 10 + # Wait a week before proposing a new release: a compromised or broken + # version is usually caught and yanked within days, and nothing here is + # urgent enough to want a same-day bump. + cooldown: + default-days: 7 groups: # Batch routine dev-dependency bumps into one PR to cut noise. dev-dependencies: @@ -16,3 +21,5 @@ updates: directory: "/" schedule: interval: weekly + cooldown: + default-days: 7 diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 31d38a7..ad6a934 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,30 +1,65 @@ name: CI -# When to run: on every push to main and on every pull request. +# When to run: on every push to main, on every pull request, and on demand. on: push: branches: [main] pull_request: + workflow_dispatch: + +# Least privilege: this workflow only ever reads the repo. +permissions: + contents: read + +# A new push to the same branch cancels the previous, still-running check. +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true jobs: check: + name: Lint, typecheck, test, build runs-on: ubuntu-latest steps: - # 1. Get your code onto the runner. + # 1. Get the code onto the runner. # Actions are pinned to a full commit SHA (org policy); comment tracks the version. - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + # No checks here push anything, so don't leave a credential in .git/config. + persist-credentials: false - # 2. Install Node 20 and cache npm downloads for faster runs. - - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 + # 2. Install the toolchain from .mise.toml — Node is pinned there to the + # same version packaging/assemble.sh bundles into the .deb, so CI tests + # the runtime that actually ships. + - uses: jdx/mise-action@7e36c90d9ab29c415a2384db3006f3ec8a8cc654 # v4.2.4 with: - node-version: 20 - cache: npm + install: true + cache: true + + # 3. Cache npm downloads across runs, keyed on the lockfile. + - uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 + with: + path: ~/.npm + key: ${{ runner.os }}-npm-${{ hashFiles('package-lock.json') }} + restore-keys: ${{ runner.os }}-npm- + + # 4. Install from the lockfile (clean, reproducible). + - run: mise run install - # 3. Install dependencies from the lockfile (clean, reproducible). - - run: npm ci + # 5. The gate. Identical to `mise run ci` locally — format check, + # both typechecks, tests with the coverage floor, and the GUI build. + - run: mise run ci - # 4. The same checks you run locally — if any fail, the build goes red. - - run: npm run typecheck - - run: npm run typecheck:node - - run: npm test - - run: npx prettier --check "src/**/*.{ts,tsx}" "gateway/**/*.ts" + zizmor: + name: Zizmor (GitHub Actions audit) + runs-on: ubuntu-latest + permissions: + contents: read + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + - name: Audit GitHub Actions workflows (zizmor) + uses: zizmorcore/zizmor-action@3dc1ecc9bcb9e94e9b2c709687979e1298497054 # v0.6.2 + with: + advanced-security: false diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 5cd0e83..11c1623 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -22,6 +22,11 @@ jobs: steps: # Actions pinned to a full commit SHA (org policy); comment tracks the version. - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + # Nothing here pushes with git — the release upload below authenticates + # with an explicit GH_TOKEN — so don't leave a credential in .git/config + # where the docker build step could pick it up. + persist-credentials: false # The package version comes from package.json (nfpm ${SEMVER}); keep the tag # in lockstep so the Release, the .deb, and the in-app version banner agree. diff --git a/.gitignore b/.gitignore index ef4f47e..05c6ca2 100644 --- a/.gitignore +++ b/.gitignore @@ -3,6 +3,7 @@ dist dist-gateway build dist-deb +coverage *.local .DS_Store gateway/.plotter-state.json diff --git a/.mise.toml b/.mise.toml new file mode 100644 index 0000000..a4a7d62 --- /dev/null +++ b/.mise.toml @@ -0,0 +1,74 @@ +# mise: https://mise.jdx.dev/ +# +# Single source of truth for the toolchain and the task list. CI runs the same +# `mise run ci` gate (see .github/workflows/ci.yml), so local and CI cannot drift. +# +# Node is pinned to the exact version packaging/assemble.sh bundles into the +# .deb, so what CI tests is the runtime that actually ships to the Pi. If you +# bump NODE_VERSION there, bump it here too. + +[tools] +node = "22.20.0" +zizmor = "1.29.0" # GitHub Actions security audit +actionlint = "1.7.12" # GitHub Actions schema + expression lint + +[tasks.install] +description = "Install dependencies from the lockfile" +run = "npm ci" + +[tasks.dev] +description = "Vite dev server for UI work on :5173" +run = "npm run dev" + +[tasks.gateway] +description = "Run the plotter gateway daemon on :8717" +run = "npm run gateway" + +[tasks.build] +description = "Typecheck + build the GUI into dist/" +run = "npm run build" + +[tasks.test] +description = "Run the unit tests, enforcing the coverage floor" +run = "npm test" + +[tasks.typecheck] +description = "Type-check the browser sources" +run = "npm run typecheck" + +[tasks.typecheck-node] +description = "Type-check the gateway sources" +run = "npm run typecheck:node" + +[tasks.format] +description = "Format with Prettier" +run = "npm run format" + +[tasks.format-check] +description = "Check formatting without rewriting files" +run = "npm run format:check" + +[tasks.audit] +description = "Security-audit the workflows + dependabot config (zizmor)" +# The token enables the online audits (impostor-commit, known-vulnerable-actions). +# Without one zizmor falls back to offline and those two simply do not run. +run = "GH_TOKEN=$(gh auth token 2>/dev/null) zizmor --collect=all --strict-collection ." + +[tasks.lint-actions] +description = "Lint the workflows for schema + expression errors (actionlint)" +run = "actionlint" + +[tasks.ci] +description = "Full CI gate — exactly what CI runs" +depends = ["format-check", "typecheck", "typecheck-node", "test", "build"] + +[tasks.ci-watch] +description = "Watch GitHub Actions CI for the current branch; exits non-zero on failure" +run = """ +sleep 5 +remote_url=$(git remote get-url upstream 2>/dev/null || git remote get-url origin) +repo=$(echo "$remote_url" | sed -E 's|^.*github\\.com[:/]||; s|\\.git$||') +run_id=$(gh run list --repo "$repo" --branch "$(git branch --show-current)" --limit 1 --json databaseId --jq '.[0].databaseId') +if [ -z "$run_id" ]; then echo "No run found for current branch on $repo" >&2; exit 1; fi +gh run watch --repo "$repo" --exit-status "$run_id" +""" diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..0b7fe25 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,120 @@ +# AGENTS.md + +This file (`AGENTS.md`) is the canonical agent configuration. `CLAUDE.md` is a symlink to this file. + +Browser-based control app for a GRBL-style pen plotter (a UUNA TEK 3.0 with an A0 +bed). A long-running gateway daemon owns the serial port and streams plots +autonomously; the browser is a thin WebSocket client. See [README.md](README.md) +for what it does and how to run it. + +## Safety — this code drives a physical machine + +These are not style preferences. Getting them wrong wastes a sheet of paper, or +drives the gantry into the frame. + +**Never interrupt a running plot.** A plot is a one-shot physical job and there is +**no resume** — a client cannot re-attach to a plot it did not start. While the +machine is moving (the state file's mtime is within ~3 s), do not: + +- `systemctl restart plotter-gateway`, run `deploy.sh`, or install an update — restarting the daemon aborts the plot. +- Open a WebSocket to the gateway. When no client holds control, the next client to connect inherits it. +- Suggest pressing **Plot**. `streamProgram` has no in-progress guard, so a second program interleaves into the running queue. + +To check progress without touching the plot, read +`/var/lib/penplotter271/.plotter-state.json` over SSH: `wpos` is the live work +position, `z: 0` is pen-down (drawing), `z: 2` is pen-up (travel). That connects +no client and transfers no control. Defer every fix and restart until the plot +finishes. + +**No limit switches** (`$22=0`, homing disabled). There is no `$H`. The operator +sets work zero by hand at the paper's top-left corner each session. After any +power cycle the restored origin can be ~1 cm off — and if it is wrong, nothing +stops the machine. + +**Machine conventions**, baked into the G-code generator: + +- **Inverted Z:** `Z+` moves the pen **down**. Pen-down Z is positive (default `3`), pen-up is `0`. +- **Origin = the paper's top-left corner**, and the SVG→G-code mapping is **identity — no Y flip**. Machine `+Y` runs physically *down* the page. Drawing fills the `+X`/`+Y` quadrant. +- **The daemon opens the serial port exactly once.** Repeated reopen wedges the macOS CH340 driver (errno 22) and only a physical replug recovers it. Never add a reopen path. + +## Architecture + +The GRBL engine depends only on a `Transport` interface — never on Web Serial, the +DOM, or React — so the same engine runs on the Pi behind a Node serial adapter. +Keep that seam intact. + +``` +src/grbl/ Portable GRBL protocol engine: streaming, status, alarms (no UI deps) +src/transport/ The seam — Transport interface + the browser's WebSocket client +src/gateway/ Shared WebSocket protocol (commands, snapshot, forwarded events) +src/plot/ Pure pipeline: SVG/PNG → polylines → placement → G-code +src/ui/ React app (the only DOM-aware layer) +gateway/ Raspberry Pi / dev daemon +``` + +`src/plot/` and `src/grbl/` are the pure, unit-tested core and the only code the +coverage floor measures. New logic belongs there rather than in `src/ui/` wherever +that is a real choice. + +## Development commands + +Use `mise`. It pins the toolchain — including Node `22.20.0`, the same version +`packaging/assemble.sh` bundles into the `.deb` — and CI runs the same tasks, so +local and CI cannot drift. + +```bash +mise install # install the pinned toolchain +mise run install # npm ci +mise run ci # the full gate: format-check, both typechecks, test, build +``` + +| Task | What it does | +| --- | --- | +| `mise run dev` | Vite dev server for UI work on :5173 | +| `mise run gateway` | Run the plotter gateway daemon on :8717 | +| `mise run build` | Typecheck + build the GUI into `dist/` | +| `mise run test` | Unit tests, enforcing the coverage floor | +| `mise run typecheck` | Type-check the browser sources | +| `mise run typecheck-node` | Type-check the gateway sources | +| `mise run format` / `format-check` | Prettier write / check | +| `mise run audit` | Security-audit the workflows + dependabot config (zizmor) | +| `mise run lint-actions` | Lint the workflows (actionlint) | +| `mise run ci-watch` | Watch the GitHub Actions run for the current branch | + +Coverage is measured over `src/plot` and `src/grbl` only, with per-metric floors in +`vite.config.ts`. Raise them as coverage improves; never lower one to make CI pass. + +## Spec-driven changes + +Non-trivial work goes through OpenSpec: proposals and tasks under +`openspec/changes/`, capability specs under `openspec/specs/`, completed changes in +`openspec/changes/archive/`. Use the `/opsx:*` skills (`propose`, `apply`, `archive`, +`sync`, `explore`). There are currently no active changes. + +Hardware-dependent tasks are **not** done when the code typechecks. Several +position-restore bugs passed review and failed on the actual Pi. Leave hardware +verification tasks unchecked until the operator confirms them on the machine. + +## Agent skills + +### Git remote + +GitHub, via the `gh` CLI. The repo is `LAB271/labs-pen-plotter`. + +**The maintainer handles all git operations themselves.** Do not commit, push, or +open PRs unless explicitly asked in that instance. + +### Issue tracker + +GitHub Issues, via `gh`. See [`docs/agents/issue-tracker.md`](docs/agents/issue-tracker.md). + +### Triage labels + +`needs-triage`, `needs-info`, `ready-for-agent`, `ready-for-human`, `wontfix`. See +[`docs/agents/issue-tracker.md`](docs/agents/issue-tracker.md). + +### Commits + +[Conventional Commits](https://conventionalcommits.org/) — see +[CONTRIBUTING.md](CONTRIBUTING.md). Explain *why*, not *what*: this codebase carries +unusually detailed inline rationale and that is deliberate. diff --git a/CLAUDE.md b/CLAUDE.md new file mode 120000 index 0000000..47dc3e3 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1 @@ +AGENTS.md \ No newline at end of file diff --git a/README.md b/README.md index fefbfc8..1ff921c 100644 --- a/README.md +++ b/README.md @@ -194,7 +194,15 @@ downloads and installs the latest release `.deb` itself, then restarts — no SS app reconnects and shows the new version. To upgrade or **roll back** by hand, install a specific `.deb` (config and state are preserved): +```bash +# Grab any version's .deb from the Releases page, then on the Pi: +sudo apt install --allow-downgrades ./penplotter271__arm64.deb +sudo systemctl restart plotter-gateway +``` +`--allow-downgrades` is what lets apt go backwards to an older version; it is +harmless when upgrading. Never do this while a plot is running — restarting the +daemon aborts it, with no way to resume. ### Building the package @@ -212,30 +220,49 @@ they build from a repo checkout on the Pi instead of installing a versioned arti ## Scripts -| Command | What it does | -| --- | --- | -| `npm run dev` | Vite dev server (UI work) | -| `npm run build` | Typecheck + build the GUI into `dist/` | -| `npm run gateway` | Run the plotter gateway daemon | -| `npm run gateway:smoke` | Hardware smoke test (moves the machine — set work zero first) | -| `npm test` | Run the unit test suite (Vitest) | -| `npm run typecheck` | Type-check the browser sources | -| `npm run typecheck:node` | Type-check the gateway sources | -| `npm run format` | Format with Prettier | +[mise](https://mise.jdx.dev/) pins the toolchain — including the exact Node version the +`.deb` bundles — and CI runs the same tasks, so local and CI can't drift: + +```bash +mise install # install the pinned toolchain (Node, zizmor, actionlint) +mise run install # npm ci +mise run ci # the full gate: format-check, both typechecks, test, build +``` + +| Command | npm equivalent | What it does | +| --- | --- | --- | +| `mise run dev` | `npm run dev` | Vite dev server (UI work) | +| `mise run build` | `npm run build` | Typecheck + build the GUI into `dist/` | +| `mise run gateway` | `npm run gateway` | Run the plotter gateway daemon | +| — | `npm run gateway:smoke` | Hardware smoke test (moves the machine — set work zero first) | +| `mise run test` | `npm test` | Run the unit test suite with coverage (Vitest) | +| `mise run typecheck` | `npm run typecheck` | Type-check the browser sources | +| `mise run typecheck-node` | `npm run typecheck:node` | Type-check the gateway sources | +| `mise run format` | `npm run format` | Format with Prettier | +| `mise run format-check` | `npm run format:check` | Check formatting without rewriting | +| `mise run audit` | — | Security-audit the workflows (zizmor) | +| `mise run lint-actions` | — | Lint the workflows (actionlint) | +| `mise run ci-watch` | — | Watch the CI run for the current branch | ## Testing ```bash -npm test +npm test # or: mise run test ``` Unit tests cover the pure, testable core — GRBL line parsing and streaming, SVG/PNG flattening and iso-contour tracing, placement and fit math, the detail thinner, and G-code generation (including the plot-time estimate). +Coverage is measured over `src/plot` and `src/grbl` only — the framework-free core. +`src/ui` and `src/transport` need a DOM and a live socket, so including them would +only produce a floor low enough to be meaningless. The per-metric floors live in +`vite.config.ts`; raise them as coverage improves, and never lower one to make CI +pass. + ## Tech stack -React 18 · TypeScript · Vite · Tailwind CSS · Konva (canvas) · `serialport` + `ws` + `tsx` +React 19 · TypeScript · Vite · Tailwind CSS · Konva (canvas) · `serialport` + `ws` + `tsx` (gateway) · Vitest ## Hardware @@ -245,7 +272,9 @@ change history for each feature live under `openspec/`. ## Contributing -See [CONTRIBUTING.md](CONTRIBUTING.md). +See [CONTRIBUTING.md](CONTRIBUTING.md) and the [Code of Conduct](CODE_OF_CONDUCT.md). +AI agents: see [AGENTS.md](AGENTS.md) — it carries the machine-safety rules that +matter before you touch anything that moves the gantry. ## Security diff --git a/SECURITY.md b/SECURITY.md index 1a8a850..f84e2c2 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -29,7 +29,7 @@ policy - including credit, the hall of fame, and the bounty - is at , and takes precedence over this document on anything it covers. -**On GitHub:** private vulnerability reporting should be enabled on this repository - use +**On GitHub:** private vulnerability reporting is enabled on this repository - use the ["Report a vulnerability" button](../../security/advisories/new) under the Security tab. diff --git a/docs/agents/issue-tracker.md b/docs/agents/issue-tracker.md new file mode 100644 index 0000000..123e8ea --- /dev/null +++ b/docs/agents/issue-tracker.md @@ -0,0 +1,63 @@ +# Issue tracker: GitHub + +Issues for this project are managed as GitHub issues. + +The issues live in the same remote as the source code (the GitHub default): +[`LAB271/labs-pen-plotter`](https://github.com/LAB271/labs-pen-plotter/issues). + +Use the `gh` CLI for all operations. Learn about it with `gh issue --help`. + +```bash +gh issue list --label needs-triage +gh issue view +gh issue create --title "..." --body "..." --label needs-triage +gh issue edit --add-label ready-for-agent --remove-label needs-triage +``` + +Do not report security issues here — see [SECURITY.md](../../SECURITY.md) for the +private disclosure route. + +## Labels + +The following issue labels are used: + +``` +NAME COLOR DESCRIPTION +bug #d73a4a Something isn't working +documentation #0075ca Improvements or additions to documentation +duplicate #cfd3d7 This issue or pull request already exists +enhancement #a2eeef New feature or request +good first issue #7057ff Good for newcomers +help wanted #008672 Extra attention is needed +invalid #e4e669 This doesn't seem right +question #d876e3 Further information is requested +needs-triage #e6e6fa Maintainer needs to evaluate this issue +needs-info #e6e6fa Waiting on reporter for more information +ready-for-agent #e6e6fa Fully specified, ready for an autonomous agent +ready-for-human #e6e6fa Requires human implementation +wontfix #ffffff This will not be worked on +``` + +Dependabot also applies `dependencies` (#0366d6), `javascript` (#168700) and +`github_actions` (#000000) to the pull requests it opens. Those are set by +Dependabot, not by hand. + +## Triage flow + +1. A new issue gets `needs-triage`. +2. The maintainer evaluates it and either closes it (`wontfix`, `duplicate`, + `invalid`) or classifies it (`bug`, `enhancement`, `documentation`). +3. If the report is incomplete, apply `needs-info` and ask the reporter. +4. Once the work is fully specified, replace `needs-triage` with either + `ready-for-agent` (an autonomous agent can implement it from the issue text + alone) or `ready-for-human`. + +Prefer `ready-for-human` for anything that needs the physical machine. Hardware +behaviour on this project cannot be verified from code review — see the hardware +verification note in [AGENTS.md](../../AGENTS.md). + +## Relationship to OpenSpec + +Substantial features are tracked as OpenSpec changes under `openspec/changes/`, +not as issues. Use issues for bug reports, small enhancements, and anything +incoming from outside the team; use `/opsx:propose` when the work needs a spec. diff --git a/package-lock.json b/package-lock.json index 2b3b0f6..0a6af66 100644 --- a/package-lock.json +++ b/package-lock.json @@ -23,6 +23,7 @@ "@types/react-dom": "^19.2.4", "@types/w3c-web-serial": "^1.0.6", "@vitejs/plugin-react": "^6.0.5", + "@vitest/coverage-v8": "^4.1.10", "esbuild": "^0.28.1", "prettier": "^3.9.6", "tailwindcss": "^4.3.3", @@ -31,6 +32,66 @@ "vitest": "^4.1.10" } }, + "node_modules/@babel/helper-string-parser": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz", + "integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz", + "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.8", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.8.tgz", + "integrity": "sha512-E8lTAYNB1KW+FH+VGJuZM1ioAx2E6oVlvQFRrf5P8ZZmsiJXYAD9vTFV7yyEURNzgh1dFqMZuO6tUwcARbqFCA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.8" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.8", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.8.tgz", + "integrity": "sha512-Vj1jF3cPfxg7OAfoI7QnVKLoILlm2JF9pnVHrX8qx7AHMiYWT+NDAA7jChlNgRS4WTLc/fD1lXLmPixluj+3Gg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@bcoe/v8-coverage": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-1.0.2.tgz", + "integrity": "sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, "node_modules/@emnapi/core": { "version": "1.11.1", "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.11.1.tgz", @@ -1808,6 +1869,37 @@ } } }, + "node_modules/@vitest/coverage-v8": { + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-4.1.10.tgz", + "integrity": "sha512-IM49HmthevbgAO4anp1hwtoT9wYe59w0LR00gr+eagHE+ZJ5lK4sLPeO0ubgoJcwLk6dehU3R24N+FbEEKDc8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@bcoe/v8-coverage": "^1.0.2", + "@vitest/utils": "4.1.10", + "ast-v8-to-istanbul": "^1.0.0", + "istanbul-lib-coverage": "^3.2.2", + "istanbul-lib-report": "^3.0.1", + "istanbul-reports": "^3.2.0", + "magicast": "^0.5.2", + "obug": "^2.1.1", + "std-env": "^4.0.0-rc.1", + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@vitest/browser": "4.1.10", + "vitest": "4.1.10" + }, + "peerDependenciesMeta": { + "@vitest/browser": { + "optional": true + } + } + }, "node_modules/@vitest/expect": { "version": "4.1.10", "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.10.tgz", @@ -1931,6 +2023,18 @@ "node": ">=12" } }, + "node_modules/ast-v8-to-istanbul": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/ast-v8-to-istanbul/-/ast-v8-to-istanbul-1.0.5.tgz", + "integrity": "sha512-UPAgKJFSEGMWSDr3LX4tqnAb4f7KGT8O40Tyx8wbYmmZ/yn58lNCm8h3svs3eXgiGd5AXxz8NDOvXWvicq+rJA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.31", + "estree-walker": "^3.0.3", + "js-tokens": "^10.0.0" + } + }, "node_modules/chai": { "version": "6.2.2", "resolved": "https://registry.npmjs.org/chai/-/chai-6.2.2.tgz", @@ -2102,6 +2206,62 @@ "dev": true, "license": "ISC" }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/html-escaper": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", + "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", + "dev": true, + "license": "MIT" + }, + "node_modules/istanbul-lib-coverage": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", + "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-report": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", + "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "istanbul-lib-coverage": "^3.0.0", + "make-dir": "^4.0.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-reports": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.2.0.tgz", + "integrity": "sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "html-escaper": "^2.0.0", + "istanbul-lib-report": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/its-fine": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/its-fine/-/its-fine-2.0.0.tgz", @@ -2133,6 +2293,13 @@ "jiti": "lib/jiti-cli.mjs" } }, + "node_modules/js-tokens": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-10.0.0.tgz", + "integrity": "sha512-lM/UBzQmfJRo9ABXbPWemivdCW8V2G8FHaHdypQaIy523snUjog0W71ayWXTjiR+ixeMyVHN2XcpnTd/liPg/Q==", + "dev": true, + "license": "MIT" + }, "node_modules/konva": { "version": "10.3.0", "resolved": "https://registry.npmjs.org/konva/-/konva-10.3.0.tgz", @@ -2436,6 +2603,34 @@ "@jridgewell/sourcemap-codec": "^1.5.5" } }, + "node_modules/magicast": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/magicast/-/magicast-0.5.4.tgz", + "integrity": "sha512-llBEhWm1SacoRwgHUoQJYtwp4PBLF4faQi5TCpIGyGs9n4y5+juI0tDgyKIfpqxckRHaHzouUEph3THklWh03w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.7", + "@babel/types": "^7.29.7", + "source-map-js": "^1.2.1" + } + }, + "node_modules/make-dir": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", + "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", @@ -2673,6 +2868,19 @@ "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==", "license": "MIT" }, + "node_modules/semver": { + "version": "7.8.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", + "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/serialport": { "version": "13.0.0", "resolved": "https://registry.npmjs.org/serialport/-/serialport-13.0.0.tgz", @@ -2749,6 +2957,19 @@ "dev": true, "license": "MIT" }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/tailwindcss": { "version": "4.3.3", "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.3.3.tgz", diff --git a/package.json b/package.json index ebf8f03..478c373 100644 --- a/package.json +++ b/package.json @@ -10,11 +10,12 @@ "gateway": "tsx gateway/server.ts", "gateway:smoke": "tsx gateway/smoke.ts", "build:gateway": "node scripts/build-gateway.mjs", - "test": "vitest run", + "test": "vitest run --coverage", "typecheck": "tsc --noEmit", "typecheck:node": "tsc -p tsconfig.node.json", "lint": "tsc --noEmit", - "format": "prettier --write \"src/**/*.{ts,tsx}\" \"gateway/**/*.ts\"" + "format": "prettier --write \"src/**/*.{ts,tsx}\" \"gateway/**/*.ts\"", + "format:check": "prettier --check \"src/**/*.{ts,tsx}\" \"gateway/**/*.ts\"" }, "dependencies": { "@types/ws": "^8.18.1", @@ -32,6 +33,7 @@ "@types/react-dom": "^19.2.4", "@types/w3c-web-serial": "^1.0.6", "@vitejs/plugin-react": "^6.0.5", + "@vitest/coverage-v8": "^4.1.10", "esbuild": "^0.28.1", "prettier": "^3.9.6", "tailwindcss": "^4.3.3", diff --git a/vite.config.ts b/vite.config.ts index 4a49479..d4f648a 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -13,6 +13,25 @@ export default defineConfig({ test: { environment: 'node', globals: true, + coverage: { + provider: 'v8', + reporter: ['text-summary', 'lcov'], + // Scoped to the pure, framework-free core — the part that is actually + // unit-testable. src/ui and src/transport need a DOM and a live socket, + // so folding them in would only produce a floor low enough to be + // meaningless. Widen this when those grow real tests. + include: ['src/plot/**', 'src/grbl/**'], + exclude: ['**/__tests__/**'], + // Set just under the coverage measured when this gate went in, so it + // ratchets against regressions rather than blocking today's work. + // Raise these as coverage improves; never lower them to make CI pass. + thresholds: { + statements: 50, + branches: 48, + functions: 40, + lines: 50, + }, + }, }, });