Guidance for Claude Code sessions working in this repo. Read this before making changes.
This is a two-level npm project:
sliderule-web-client/ ← git root, outer wrapper
├── package.json ← husky, Playwright orchestration
├── package-lock.json
├── Makefile ← canonical interface (use this!)
├── web-client/ ← the Vue 3 app
│ ├── package.json ← runtime deps, Vite, TypeScript, ESLint
│ ├── package-lock.json
│ ├── src/ ← all app source
│ ├── tests/ ← Vitest unit + Playwright E2E
│ └── playwright.config.mts
├── terraform/ ← infra (CloudFront + S3)
├── keycloak/ ← local OAuth dev harness
└── docs/
Both package.jsons require installs. Both are guarded with engines +
packageManager + .npmrc (engine-strict=true).
The Makefile is the single source of truth. CI calls it; local dev should too.
Prefer make <target> over npm run ... / vite ... / npx ... directly.
Key targets:
| Task | Command | Notes |
|---|---|---|
| Install npm deps | make install-deps |
Runs npm ci at root and web-client/ |
| Reinstall npm deps (clean) | make reinstall-deps |
clean-all + install-deps |
| Full rebuild from scratch | make rebuild-all |
reinstall-deps + build |
| Regenerate lockfiles | make regen-lockfiles |
Destructive, rarely needed |
| Verify env | make doctor |
Shows Node/npm vs pinned versions |
| Verify lockfiles (fast) | make check-lockfiles |
npm ci --dry-run; no reinstall, ~1s |
| Verify lockfiles (full) | make verify-lockfiles |
Mirrors CI; reinstalls both node_modules |
| Dev server | make run |
NOT npm run dev |
| Production build | make build |
NOT vite build — Makefile injects VITE_APP_VERSION etc. |
| Preview build | make preview |
|
| Typecheck | make typecheck |
|
| Lint / fix | make lint / make lint-fix |
|
| Unit tests | make test-unit |
Vitest |
| E2E tests | make test-e2e |
Playwright (runs from web-client/) |
| Lint the CloudFormation template | make lint-cfn |
Pinned cfn-lint via uv; nothing installed, no AWS |
| All CI checks | make ci-check |
|
| Full list | make help |
- Node — version pinned in
.nvmrc. Enforced viaengines+engine-strict=true. Use fnm or nvm; Homebrew Node works but doesn't respect Corepack. - npm — version pinned in
packageManagerfield (both package.json files). Corepack fetches the exact version on contributors' machines. Requirescorepack enable && corepack enable npm. - Never run
npm installwithout intent.make install-deps(which wrapsnpm ci) is the default.npm installre-resolves and rewrites lockfiles — CI will catch this via the drift check.
.gitattributes normalizes line endings to LF and marks
images/fonts/wasm/parquet as binary. When adding a new binary asset type, add
an explicit rule there.
sliderule.slideruleearth.io is hardcoded intentionally as the permanent
public API server. Do not parameterize or "generalize" it without discussing
the architectural implications first.
Three hosts, with different jobs:
| Host | Serves | Repo |
|---|---|---|
slideruleearth.io |
nothing — 301s / to the client, 404s everything else |
this one (terraform/) |
client.slideruleearth.io |
the Vue SPA | this one |
docs.slideruleearth.io |
the documentation | a different one |
The apex hosts nothing. A viewer-request CloudFront function in
terraform/modules/cloudfront.tf answers
every request at the edge: / gets a 301 to <domainName>/landing, and every
other path gets a plain-text 404. The distribution's S3 origin exists only
because CloudFront requires one; it is never reached.
Do not add an allowlist of paths that pass through to the origin. That was tried (PR #1094, first revision) and reverted: the apex has no content to describe, so anything published there would be describing the client or the docs from a host that serves neither.
Machine-readable files for agents belong on docs.slideruleearth.io,
which is where the actual content is and is maintained in a separate
repository. The web client is a single-page app — client.slideruleearth.io
returns a 385-byte empty shell, versus ~115 KB of rendered prose from the docs
site — so a sitemap or an llms.txt pointing at it would index nothing.
llms.txt is not implemented on any SlideRule host, and adding one here is
not a pending task.
web-client/public/robots.txt is the one
crawler-facing file this repo publishes, and it answers for
client.slideruleearth.io only (the apex 404s its own /robots.txt). Vite
copies public/ into dist/ verbatim, so editing the file and deploying is
the whole workflow:
make live-update-slideruleearth(DOMAIN_APEX is the one per-environment input; DOMAIN is derived as
client.<apex> and check-vars refuses a value that disagrees. The wrapper
above is make live-update DOMAIN_APEX=slideruleearth.io S3_BUCKET=slideruleearth-webclient.)
Its Disallow rules mirror the router. Adding a per-session route (something
under /analyze/, /request/<id>, /auth/) means adding it there too.
make upload-robots, which live-update runs, is the sole publisher of
this file — upload-static excludes it deliberately. dist/robots.txt is
always the production, crawlable file, so without that exclusion every deploy
would publish it first and only then have upload-robots overwrite it. On a
staging bucket that puts the production policy live for the length of the
deploy, and leaves it live if the second upload fails.
The exclusion does not make a failed upload-robots harmless. A failed
aws s3 cp leaves whatever the bucket already had: the previous deploy's
noindex file, or nothing at all on a fresh bucket. Nothing is not safe either
— the client distribution answers a missing key with /index.html at status
200 (the 403→200 custom_error_response), so a crawler asking for
robots.txt gets HTML with no Disallow in it. After a first deploy to a new
bucket, check that robots.txt actually landed.
upload-robots also sets an explicit Content-Type and a 300-second
max-age, because aws s3 sync guesses types from the extension and never
sets a charset.
Off production it substitutes robots.noindex.txt — a
bare Disallow: /. The discriminator is DOMAIN, the client host, not
DOMAIN_APEX, because a non-production client can sit under the production
apex and keying on the apex would publish the crawlable file to it. Anything
that is not exactly client.slideruleearth.io gets the noindex file, so an
unrecognised or mistyped DOMAIN fails safe. The deploy log says which file
it used.
public/ is copied into dist/ verbatim, dot-directories included, and
upload-static syncs dist/ to the bucket. A .DS_Store that reached
public/ was publicly served until 2026-09-02; upload-static now excludes
*.DS_Store, but the real fix is not to put anything there that is not meant
to be a public URL.
The web client does not support the Model Context Protocol, and no MCP server
ships with this project. Earlier experiments were never merged; the branches
and the sliderule-mcp-server/ directory that held them were deleted on
2026-09-02, and there is no web-client/src/services/ directory on main.
Do not describe MCP as a capability of the client or the deployed site, and do not reintroduce it without asking first.
make buildproducesweb-client/dist/- Content deploys are
make live-update-<env>(build, upload, invalidate, verify). These are the everyday path and are unchanged by the migration. - Infrastructure is mid-migration from Terraform to CloudFormation
(
docs/cloudformation-migration-plan.md,cloudformation/README.md, issue #1108). Thestack-*/bucket-*targets anddeploy-client-to-<env>/destroy-client-<env>are CloudFormation-only and refuse an environment that is still on Terraform. Until an environment's cutover its infrastructure is frozen: nothing underterraform/changes, and the one exception — an emergency change such as republishing the apex function — is run by hand fromterraform/with the workspace selected, never throughmake. Check the plan's Phase 3/4 checkboxes to see which environments have cut over. make keycloak-up/keycloak-downspin up a local OAuth server for auth dev
ESLint flat config is explicitly disabled in the lint scripts:
ESLINT_USE_FLAT_CONFIG=false. Do not "fix" this by migrating to flat config
without coordinating — the legacy config is intentional here.
- Unit: Vitest,
make test-unit, sources inweb-client/tests/ - E2E: Playwright,
make test-e2e, config inweb-client/playwright.config.mts. The CI workflow runs E2E against the production build. - Typecheck:
make typecheckcoverstsconfig.app.jsonandtsconfig.node.json. It does not covertsconfig.vitest.json— the app project excludestests/**, so test sources are type-checked bymake typecheck-tests, whichmake test-unitruns first. Between the two targets all three projects are covered; neither one covers all three alone.
The pre-commit hook is one line: make pre-commit-check, which runs
check-lockfiles, lint-staged, typecheck, then test-unit (which
type-checks the tests first) — about 15 seconds. The hook delegates to the
target deliberately, so the two cannot drift apart; run the target directly to
reproduce the hook without committing.
CI (.github/workflows/playwright.yml)
runs verify-lockfiles, typecheck, test-unit, then test-e2e. The fast
checks sit ahead of the Playwright browser install on purpose, so a type error
fails in seconds instead of minutes.
A second workflow
(.github/workflows/cloudformation.yml)
runs make lint-cfn only when cloudformation/**, the Makefile or the
workflow itself changes.
make ci-check bundles the same set plus lint and lint-cfn for local use.
Note the workflows invoke the individual targets rather than calling
ci-check, so adding a target to ci-check does not add it to CI — edit
the workflow too.
engines+engine-strict=truein both.npmrcfiles → wrong Node/npm versions fail install outrightpackageManager: npm@<pinned>in bothpackage.jsonfiles + Corepack → exact npm version fetched and usednpm cirefuses to install (EUSAGE) whenpackage.jsonandpackage-lock.jsondisagree — that refusal is the drift check.make check-lockfilessurfaces it in ~1s via--dry-run, reading both files from the index rather than from disk, because a commit ships the index and the two can differ.make verify-lockfilesadditionally does the real install and fails if it rewrites either file; it reads the working tree, and hashes the files around the install so uncommitted edits do not trip it..gitattributes→ no line-ending corruption of binaries across platforms
If you see a large, unexplained lockfile diff or a package version change that nobody intended, treat it as a bug — don't rubber-stamp the PR.