SCOPE: everything in progressREADME.md · build b00458d32828

SCOPE: safety platform POC

CI

A proof of concept for a safety and risk management platform, demonstrated on a waste-services operator: eight hauling depots, a landfill and a recycling center. Drivers file vehicle inspections (DVIRs) and incident reports on shared tablets, offline when there is no signal; supervisors, general managers and a VP work investigations, corrective actions and training from role-scoped dashboards.

Two things live here: a synthetic data set (about 840,000 rows over 24 months, deliberately planted storylines, all people fictional) and the app that captures and reports against it.

"Today" is 2026-09-28, the demo date. Every overdue, expiring and due-today flag is computed against it, never against the real clock. Four places carry the date and must agree: scripts/window.py, app.today() in SQL, TODAY in packages/core/src/config.ts, and FROM/TO in packages/core/src/cofi/facts.ts.

Layout

apps/web          Next.js 16 (App Router): every screen, tablet-first, installable PWA
packages/core     business rules and types. No React, no DOM, no database client
packages/db       data access: Supabase queries and RPC wrappers
supabase/         migrations (102) and local config
data/csv          the generated sample data set (32 CSVs). Never hand-edit
data/policy       the client rule book, extracted into CSVs
scripts/          data generator and its verification suites
scripts/db        load, test and demo-setup scripts for a live database (see its README)
scripts/cofi      the COFI reporting layer generator and checks
scripts/brand     binds one client's branding into a build
tenants/          per-client branding, one folder per client (see its README)
vendor/           a Power BI project kept as a reference (see its README)
docs/             working documents; docs/archive is superseded, docs/internal is ours

Architecture in one paragraph

The browser talks to Supabase (Postgres, Auth, Storage, PostgREST). Row-level security on every table scopes each persona to their own records, their site, or their sites; reporting functions run as the caller, so a filter in the UI can only narrow what the database already allowed. Writes that must be atomic or must not trust the device (capture, sign-off, assignment) go through Postgres functions, never Server Actions, so the same calls work from a future native app. Business rules live in packages/core and are shared by the UI and the offline DVIR, which queues captures in an IndexedDB outbox and uploads them when signal returns. CLAUDE.md holds the full set of decisions and the constraints that produce wrong output when violated.

In the repo but not in the product

  • Ask (/ask, packages/core/src/ask, the ask_log table): a future feature. A question box over the safety data that answers from a fixed catalog of tested queries, with no model. Built and tested, switched off: nothing links to it and its routes redirect home.
  • Leads (/hub/leads, the sales schema): an internal tool for the team selling SCOPE. Not part of the product; reachable only from the internal hub.
  • Demo machinery (the persona rail, the demo strip, /hub, the demo reset and its demo_baseline schema): what runs the demo. Not part of a client deployment's product.

Running the app

npm install
npm run dev
npm test          # every unit suite; also run by CI on each push

Getting started also means the local checks, once per clone (brew install pre-commit):

pre-commit install --allow-missing-config

On commit, in seconds: file hygiene, Ruff, Gitleaks, ESLint, and the project guards (no hand-edited data/csv/, the four demo-date anchors agree, a new table enables RLS and its audit columns, no two migrations share a version). The push stage (type-check, unit suites, data suites) is opt-in, since CI runs it anyway: pre-commit install --hook-type pre-push. CI runs the same file and is the one that counts; what runs where, and why, is docs/precommit-and-ci.md.

Environment: .env.local at the repo root (used by the scripts) and apps/web/.env.local (used by the browser). Neither is committed.

Root .env.local
SUPABASE_DB_URL Postgres connection string, percent-encoded
SUPABASE_URL, SUPABASE_PUBLISHABLE_KEY
NEXT_PUBLIC_SUPABASE_URL, NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY mirrored into apps/web/.env.local

Verifying the data

None of these need a database. All four must report zero failures, and a generator change is not finished until they do and two runs are byte-identical.

python3 scripts/generate_sample_data.py   # regenerate; deterministic, seeded from the anchor date
python3 scripts/audit.py                  # foreign keys, logic checks, realism benchmarks
python3 scripts/verify.py                 # file hygiene, vocabulary, uniqueness, org structure
python3 scripts/coherence.py              # narrative coherence; prints whole records
python3 scripts/precheck_ddl.py           # every DDL constraint asserted against the CSVs

Rebuilding the database

supabase db push --db-url "$SUPABASE_DB_URL"
scripts/db/load.sh --reset

The demo personas, their storylines and the checks that prove the rules as real logins come after that; the full sequence is the Commands section of CLAUDE.md, and scripts/db/README.md says what each script is for.

The database holds state the CSVs do not. Some reference tables are created and seeded by migrations, and load.sh --reset empties several of them, which fails open: the certification gate stops gating and says nothing. load.sh therefore runs scripts/db/reseed_reference.sql at the end of every load. Never assume a reloaded database is a complete one.

Scale

10 sites · 601 people · 396 assets · 1,492 incidents · 2024-10-01 → 2026-09-28. Real site names in Rutland County, VT; every person is synthetic.

See CLAUDE.md for decisions and constraints, and data/README.md for the data dictionary.

Back to top