Project Sidewalk is a web-based crowdsourcing tool for mapping and assessing sidewalk accessibility. Scala 2.13 +
Play 3.0 (Java 17) backend, Postgres + PostGIS via Slick, and a vanilla-JS frontend that Grunt concatenates (no
transpile, no minify, no module system), all run in Docker. Request flow is routes → Controller → Service → Table
(DAO). Architecture tour: docs/architecture.md. Setup, daily commands, troubleshooting: docs/dev-environment.md.
Real live secrets, nothing enforces this but you. Never open it, never print or commit a value from it, and exclude
it from wide grep/find/cat * sweeps. Ask the maintainer for a value; docker-compose.yml has dummy equivalents.
The moment you begin implementing or debugging an issue (not when filing, reading, or triaging it), assign the
developer you act for: gh issue edit <n> --add-assignee @me. Assignment means "someone is on this now", so it is
never done at filing time and never skipped. If someone else is already assigned, say so before adding yourself.
This file holds only cross-cutting rules. .claude/rules/ surfaces path-scoped essentials when you touch a matching
file, and this table says which doc to read first:
| Working on… | Read first |
|---|---|
A schema change (conf/evolutions/) |
docs/evolutions.md |
A /v3/api endpoint or app/models/api/ |
docs/architecture.md → "The public API" (and the update-apis skill) |
Translations (conf/messages/, public/locales/) |
docs/internationalization.md |
| CSS, Twirl views, any UI | docs/style-guide.md, docs/accessibility.md |
| A new or changed user interaction | docs/logged-events.md |
Which labels Validate serves (ValidationQueuePolicy, the Validate queries) |
docs/validation-queue.md |
| Releases, deploys, asset caching, persistent media dirs | docs/deployment-and-stages.md |
| Storing uploaded media (DB row vs. media dir) | docs/architecture.md → "Media storage" |
Label crops, or a marker drawn on one (label_crop provenance, #2660) |
docs/architecture.md → "Media storage" |
| Tests or CI | docs/testing-and-ci.md, test/e2e/README.md |
scripts/*.py |
scripts/README.md |
| Onboarding a new city (streets, regions, schema, configs) | docs/onboarding-a-city.md (and the onboard-city skill) |
| Google Maps keys, quotas, or a Google Cloud bill | docs/google-cloud.md |
| The label lat/lng estimator or the labeling viewport frame | docs/label-latlng-estimation.md |
developis the main branch and the PR target;masteris the release branch. Branch names start with the issue number (1234-fix-label-popup).- Start a ticket by branching from an up-to-date
develop, unless the maintainer says otherwise. - Never open a pull request, merge, tag, or release without the maintainer's explicit OK. Do the work, run the checks, push the branch if useful, then stop and ask. Filing GitHub issues is fine. Maintainers: @jonfroehlich and @misaugstad.
- Prod deploys are tag-triggered (
vX.Y.Zonmaster); pushingdevelopredeploys the test stage. - Edit
src/files only. Never run grunt or editbuild/output: the developer'snpm startrunsgrunt watch. A newsrc/file must match a glob inGruntfile.js. - Keep docs in sync in the same change.
docs/architecture.mdis the human-facing architecture reference; exact dependency versions live only indocs/upgrading-libraries.md. - Never browser-test anything that needs a street-view panorama (placing labels, validating). Hand the developer a
checklist or console snippet instead. The
test/e2e/suite only loads Explore's tutorial and Validate's landing state; don't extend it into pano interaction.
- Scala:
make scalafmt-fix(a blocking CI gate). Compile check without fighting the developer'ssbt ~ run:make compile.-Xfatal-warningsis on, so a success is warning-clean. - Frontend:
make lint(ESLint, Stylelint, HTMLHint, locale parity, CSS layout, asset paths, vendor versions, JS types, evolutions lint; all blocking CI gates), or scope it withmake eslint dir=…/make stylelint dir=….make lint-fixhandles the mechanical fixes. The tree is lint-clean, so any finding is from your change. - Tests:
make test-scala(needs the db container;only=<Spec>scopes it),make test-js(jsdom unit suite),make test-e2eagainst a running app,make test-python. Details and what CI gates:docs/testing-and-ci.md. - From a worktree, every
maketarget above checks that worktree, not the main checkout (make lintnames the tree), thoughmake test-e2etests whatever app is on :9000. A hand-typeddocker exec … "cd /home && …"still checks the main checkout, so use the targets.
- ES2022. As you touch code, modernize it: constructor functions →
classwith#privatefields, jQuery →fetch+ Promises, Bootstrap → native (defer a refactor that would ripple through many callers). Build HTML with template literals, never+concatenation. - Comments say why, never what. ScalaDoc (
@return) / JSDoc (@returns, typed@param) on every class and non-trivial method, including private ones. Never describe what code used to do; git history has that, and a hook flags it. Templates:docs/style-guide.md→ "Comments". - 120-char lines (exceptions where a break would hurt readability), 2-space indent. SQL: no table aliases.
Distances are geodesic (
ST_Length(geom::geography), SlicklengthGeodesic, turf.js), never via a fixed SRID. - A server-to-server POST authenticated by the internal key (
ControllerUtils.internalKeyValid) needs a+ nocsrfline above itsconf/routesentry, or the CSRF filter 403s every real request while curl tests look fine. - Mobile detection has one definition,
ControllerUtils.isMobile; JS readsutil.isMobile(). Never re-sniff the UA. - User interactions are logged (clicks, key presses, mode switches, …). When you add or change one, add or adjust
the logging and update
docs/logged-events.md. - All user-facing text is translated into every supported language (en, es, nl, de, pt-BR, zh-TW, fr, plus the
en-US/en-NZ overlays). Backend English goes in
conf/messages/messages.en, never the basemessages. Preferdata-i18n="ns:key"in HTML, including HTML built in JS. - UI meets WCAG 2.1/2.2 AA and is styled from the
main.css:roottokens and.ps-*primitives: px never rem, no hardcoded hex, tool-UI dimensionscalc(<n>px * var(--ui-scale, 1)). Full rules load with any CSS or view file. - Assets are named by logical path, never by URL:
assets.path("…")in Twirl andutil.assetPath('…')in JS, never a hardcoded/assets/string (only those resolve to the fingerprinted URL;make lint-asset-pathsgates it). A CSSurl()just names a real file underpublic/.
Domain values (enum members, ranges, thresholds, and especially the mappings between them) come from the backend, a
/v3/api/... endpoint or a value the controller passes to the view, and are never re-declared as frontend literals.
Even a "trivial" constant encodes logic: severity 1–3 maps to good/ok/bad in opposite directions for positive
features (curb ramps) and negative ones (obstacles) — which direction a type reads, or whether it's rated at all, is
its rating_scale, from /v3/api/labelTypes. Source it; if no source exists, expose one as part of the task;
only if genuinely unavoidable, centralize the literal with a comment saying why it isn't sourced.
| Label type | Color | Label type | Color | |
|---|---|---|---|---|
| CurbRamp | #90C31F |
NoSidewalk | #BE87D8 |
|
| NoCurbRamp | #E679B6 |
Crosswalk | #FABF1C |
|
| Obstacle | #78B0EA |
Signal | #63C0AB |
|
| SurfaceProblem | #F68D3E |
Other, Occlusion | #B3B3B3 |
Never invent substitute colors. JS calls util.misc.getLabelColors(labelType); /v3/api/labelTypes is canonical.
The only marker icon frontend code may use is public/images/icons/label_type_icons/{LabelType}_small.svg, reached
through util.misc.getIconImagePaths(labelType).iconImagePath. The .png sizes beside it exist for server-side
share images and the API's icon_url fields only.
- Everything runs in Docker (
make dev). The developer usually has it up: checkdocker psand reuse. The app is at http://localhost:9000;WebFetchcan't reach it, so usecurl. - Most routes need a session:
curl -s -c /tmp/sidewalk_cookies.txt "http://localhost:9000/anonSignUp?url=%2F"once, then pass-b /tmp/sidewalk_cookies.txt. Admin-role QA and running a worktree's branch (make qa-worktree wt=<name>):docs/dev-environment.md. - Inspect the DB read-only:
docker exec projectsidewalk-db psql -U readonly_user -d sidewalk -c "…"(never-U sidewalk). One schema per city (sidewalk_seattleis a safe default for schema questions), auth insidewalk_login. The active schema is$DATABASE_USERin the web container;readonly_usermay lack rights on it, and\dtsilently omits what it can't see, so enumerate viapg_namespaceand query as the city's own role. Evolutions auto-apply on page load. The dev DB is tiny and omitsaudit_task_interactionandvalidation_task_interaction; never infer prod size from it.