Engineering conventions
The code standards, architectural seams, testing strategy and working agreements for this repository. For anyone writing code here; read the nested AGENTS.md for the area you are changing as well. System structure is in architecture overview; security rules are in security.
Repository areas
| Area | Language and runtime | Rule of thumb |
|---|---|---|
src/ | React, Vite, TypeScript, shadcn-ui, Tailwind v4 | UI and thin data access; no compliance rules |
domain/ | Pure TypeScript | Zero I/O and no Supabase import; every compliance rule lives here |
supabase/functions/ | Deno edge functions | Orchestration and I/O around domain/; no rules of their own |
supabase/migrations/ | SQL | The schema and RLS; the source of truth |
db-tests/ | Vitest against local Postgres | Regression suite for isolation and ingestion, plus operational CLIs |
map-service/ | Node 22 with type stripping | Owns its wire contract in src/shared/ |
scripts/ | Node and shell | Dev, guard, seed and maintenance tooling, not application code |
archive/ holds the earlier version of the application, kept for reference. Do not build against it; it is not linted or shipped. (The domain package's legacy-fidelity tests import a few of its modules.)
The seams
Three boundaries keep rules, data access and auth in one place each. ESLint enforces the first and third; the second is enforced by review.
@complied/domain. Only domain/src/** is imported through this alias (vite.config.ts, tsconfig*.json); it is not an npm package. Compliance logic (obligation rules, order registry and decisions, form bundles, XRF classification, licence validation, status normalization, ingestion) is pure functions and types, so the browser, the edge functions and the tests all run the same code. Edge functions import it by relative path. Each folder has an index.ts that re-exports with explicit .ts extensions, and domain/src/index.ts is the public surface.
src/data/ is the only place that talks to Supabase. src/data/supabaseClient.ts holds the single createClient() call, with the anon key only. Every other module reaches the database through small, named, typed, promise-returning functions in per-entity files (projects.ts, buildings.ts, invoices.ts, ...) that call supabase.from(...) or .rpc(...) and throw descriptive errors (`listTrackedBuildings failed: ${error.message}`). Do not put .from() or .rpc() in a component, hook or page.
src/auth/ is the only place that touches supabase.auth.*. It wraps sign-in, sign-out, invites and session state behind AuthProvider, useAuth, usePermission and friends.
eslint.config.js enforces the data and auth seams for every src/**/*.{ts,tsx} file outside src/data/** and src/auth/**:
import/no-restricted-pathsforbids importingsupabaseClient.ts.- A
no-restricted-syntaxbackstop forbids any.from(...)or.rpc(...)call shape, however the client reached the call site.
Do not fight these rules; add a function in src/data/ instead. tsc -b covers src/ only, so edge functions are checked separately by npm run check:functions (Deno).
Permissions in the UI
Every action and route is gated by a permission key, never a role name: usePermission('key') or useAnyPermission(...) at the control, and src/auth/routeAccess.ts for routes (read by the route guard, sidebar, command palette and post-login landing). Keys are the typed PermissionKey union in src/auth/permissionKeys.ts, populated from my_permission_keys() so implied keys and impersonation match the server. These gates are UX; RLS is the boundary (permissions).
TypeScript and React
- Compiler flags (
tsconfig.app.json):verbatimModuleSyntax(useimport typefor types),erasableSyntaxOnly(no enums, namespaces or constructor parameter properties, which also keepsmap-service/runnable under Node's type stripping),noUnusedLocals,noUnusedParameters,noFallthroughCasesInSwitch, target ES2023.@typescript-eslint/no-unused-varsis off becausetsccovers it. - Aliases:
@/issrc/;@complied/domainisdomain/src/index.ts;@map-shared/*ismap-service/src/shared/*. - State is plain
useStateanduseEffectoversrc/data/calls, with arefreshKeycounter to refetch after a mutation (seesrc/hooks/useSavedViews.ts). There is no React Query, Zustand or Redux. The one global is auth (AuthProvider). - Routes are code-split where heavy (the map page pulls in
mapbox-gl). OneerrorElementcovers the tree, including lazy-import failures. Staff and portal are separate shells with separate guards. - Shared UI lives in
src/components/redesign/(list and detail primitives, filters, the status kit) andsrc/components/ui/(shadcn primitives). Visual rules: design system. - Display helpers that are pure belong in
src/lib/with a colocated.test.ts; keep them free ofsrc/data/imports so tests run without environment variables. - Errors thrown from
src/data/carry the function name and cause; UI code turns them into readable messages (edge-function failures go throughdescribeEdgeFunctionError).
Edge functions
- Import
withCorsand wrap the handler; authorize withauthorize(); probe ownership withcreateCallerClient(); then use the service-role client (security). - Import shared helpers from
supabase/functions/_shared/and domain code by relative path. Pinesm.shpackage versions. - A function contains no business rules: derive figures, classifications and eligibility in
domain/and call them, so the UI and the function cannot disagree (the XRF report and its ingest preview share one summary module for this reason). - New functions must pass
npm run check:functions; a function that already fails is on theKNOWN_FAILINGratchet inscripts/check-functions.mjs, and fixing one means removing it from that list.
Naming
- Files and docs: components
PascalCase.tsx; data, lib and domain modulescamelCase.ts; tests beside the code asname.test.ts; markdown underdocs/inkebab-case.md; edge-function folderskebab-case. - Data-layer functions read as verbs on entities:
listX,getX,trackBuilding,setTrackedBuildingActive. - Migrations:
YYYYMMDDHHMMSS_short_description.sql. - Permission keys are lower snake case verbs:
view_*reads,manage_*writes,perform_*does field work. - Ingestion feed ids are kebab-case (
hpd-violations);source_idvalues are namespaced by the feed's prefix. - Vocabulary follows the glossary in
REQUIREMENTS.md(for exampletenantis the organisation, not a resident); use the canonical term in code, UI text and docs.
Migrations
- Add one with
npx supabase migration new <name>; apply locally withnpx supabase migration up --local. Merging tomainapplies it to production (deployment). - Your timestamp must sort after every existing file; rebase on
mainand checkls supabase/migrations/*.sql | tail -3before opening the PR. Never rename a migration after it has been applied anywhere; that is what causes drift (local development). - Write idempotent DDL where practical (
if not exists,create or replace), and comment intent and the decision it serves in the header. - RLS: enable it, use permission-key policies with
(select ...)-wrapped helpers, and keepUSING (true)to public reference tables. EverySECURITY DEFINERfunction pinssearch_pathand ships a pairedREVOKE. Checklist: security. - Before dropping or reshaping a table, grep migrations for
REFERENCES public.<table>and for triggers writing into it. - Regenerate
src/integrations/supabase/types.tswhen the schema changes (npx supabase gen types typescript --local). - Server-to-server functions that authenticate themselves need
verify_jwt = falseinsupabase/config.toml, or the platform rejects them before your code runs.
Testing
| Layer | Tool and location | What it proves |
|---|---|---|
| Domain rules | Vitest, domain/tests/ (cd domain && npm test) | Pure logic: obligation rules, form bundles, XRF, licensing, ingestion normalizers and runners against an in-memory Db |
| Legacy fidelity | domain/tests/fidelity/ | The new pipeline matches a committed snapshot generated by running the real legacy Python over a 36-CSV corpus; deliberate divergences are asserted exactly ("we meant to differ by 4"), so an accidental change fails as loudly as an undocumented one. The raw corpus is local-only; without it, per-file assertions skip loudly and only corpus-independent invariants run. Restore the corpus rather than relaxing an assertion. |
| App and pure helpers | Vitest, src/**/*.test.ts and pure supabase/functions/** modules (npm run test) | UI logic, URL codecs, report HTML builders, retry math |
| Edge-function types | Deno, npm run check:functions | Type-correctness of every function and the domain code it imports |
| Database | Vitest, db-tests/ against local Postgres | Cross-tenant leakage matrix, permission matrix and policy lint, ingestion constraints, security fixes |
| Map service | Vitest, map-service/ | SQL compiler, fixture world in in-memory DuckDB, HTTP layer, tenant line |
Guidance:
- Put logic where it can be tested purely. A rule in
domain/gets a unit test; a rule in a component or function usually cannot be tested cheaply. - Use real rows as fixtures. Ingestion tests use rows saved from the live datasets, not invented ones; fetching is an injected dependency (
FeedFetchDeps.fetchJson), so no test calls Socrata. - Keep app unit tests off
src/data/.supabaseClient.tsthrows at import time withoutVITE_SUPABASE_URL, and CI has no env file. A type-only import of a data module is fine (erased). Before claiming a green run, move.env.localaside so it cannot mask this. - Every new tenant table gets leakage coverage in
db-tests, and a new permission key must be enforced somewhere or the policy lint fails. - A skipped or relaxed assertion is not a fix. If a failure looks pre-existing, establish the baseline on a clean checkout first.
db-testsinserts fixture rows intoauth.userson every run and never tears down; do not run it against a database holding real data without asking (local development).- CI runs the root suite, function type-check and map-service suite. The
domain/anddb-tests/suites run locally.
Commit and review cadence
The working loop is: build and tests green, commit, push; open a pull request for anything that changes shared behaviour. Cloudflare Pages auto-deploys the frontend on push to main, and merging a migration or function change deploys it (deployment).
- Write commit messages as a short imperative summary of the change ("Show which violations each dust-wipe room came from"), with detail in the body when the why is not obvious.
- Keep a change coherent: schema, policies, data-layer functions, domain rules and UI for one feature can travel together, but unrelated fixes should not.
- Update the docs that describe what you changed, in the same change. Docs describe what is true in the code today, not plans; open work belongs in issues.
Pre-commit and pre-tool gates
scripts/hooks/pre-tool-gate.mjs is a Claude Code PreToolUse hook (wired in .claude/settings.json for Bash). For agent-run commands it blocks git commit unless tsc --noEmit and npm test pass, blocks a function deploy unless npm run build passes, and blocks writes to the production Supabase database. It does not run for a human's terminal, so run npm run build and npm run test before you commit anyway. Details: security.
Never run without asking first: supabase db reset, db-tests/tests/run-tests.sh, and any destructive operation or production write (local development).
Issues and labels
Open work is tracked as GitHub issues on Martial-Geek/complied, through the gh CLI (docs/agents/issue-tracker.md). Triage uses five labels, unchanged from the defaults (docs/agents/triage-labels.md):
| Label | Meaning |
|---|---|
needs-triage | A maintainer needs to evaluate it |
needs-info | Waiting on the reporter |
ready-for-agent | Fully specified; an unattended agent can take it |
ready-for-human | Needs human implementation |
wontfix | Will not be actioned |
Domain vocabulary and locked decisions (D1 to D21) are in REQUIREMENTS.md; treat output that contradicts a locked decision as a conflict to raise, not to override (docs/agents/domain.md).
Documentation conventions
Canonical docs live in docs/ and the root REQUIREMENTS.md; the docs site copies them at build time (deployment). Each page starts with a one-to-three sentence statement of what it covers and who it is for, states current behaviour in the present tense, uses tables for reference material and Mermaid for diagrams, and links to the one page that owns a fact instead of repeating it. Link code with repo-relative paths.