Skip to main content

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​

AreaLanguage and runtimeRule of thumb
src/React, Vite, TypeScript, shadcn-ui, Tailwind v4UI and thin data access; no compliance rules
domain/Pure TypeScriptZero I/O and no Supabase import; every compliance rule lives here
supabase/functions/Deno edge functionsOrchestration and I/O around domain/; no rules of their own
supabase/migrations/SQLThe schema and RLS; the source of truth
db-tests/Vitest against local PostgresRegression suite for isolation and ingestion, plus operational CLIs
map-service/Node 22 with type strippingOwns its wire contract in src/shared/
scripts/Node and shellDev, 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/**:

  1. import/no-restricted-paths forbids importing supabaseClient.ts.
  2. A no-restricted-syntax backstop 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 (use import type for types), erasableSyntaxOnly (no enums, namespaces or constructor parameter properties, which also keeps map-service/ runnable under Node's type stripping), noUnusedLocals, noUnusedParameters, noFallthroughCasesInSwitch, target ES2023. @typescript-eslint/no-unused-vars is off because tsc covers it.
  • Aliases: @/ is src/; @complied/domain is domain/src/index.ts; @map-shared/* is map-service/src/shared/*.
  • State is plain useState and useEffect over src/data/ calls, with a refreshKey counter to refetch after a mutation (see src/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). One errorElement covers 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) and src/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 of src/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 through describeEdgeFunctionError).

Edge functions​

  • Import withCors and wrap the handler; authorize with authorize(); probe ownership with createCallerClient(); then use the service-role client (security).
  • Import shared helpers from supabase/functions/_shared/ and domain code by relative path. Pin esm.sh package 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 the KNOWN_FAILING ratchet in scripts/check-functions.mjs, and fixing one means removing it from that list.

Naming​

  • Files and docs: components PascalCase.tsx; data, lib and domain modules camelCase.ts; tests beside the code as name.test.ts; markdown under docs/ in kebab-case.md; edge-function folders kebab-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_id values are namespaced by the feed's prefix.
  • Vocabulary follows the glossary in REQUIREMENTS.md (for example tenant is 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 with npx supabase migration up --local. Merging to main applies it to production (deployment).
  • Your timestamp must sort after every existing file; rebase on main and check ls supabase/migrations/*.sql | tail -3 before 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 keep USING (true) to public reference tables. Every SECURITY DEFINER function pins search_path and ships a paired REVOKE. 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.ts when the schema changes (npx supabase gen types typescript --local).
  • Server-to-server functions that authenticate themselves need verify_jwt = false in supabase/config.toml, or the platform rejects them before your code runs.

Testing​

LayerTool and locationWhat it proves
Domain rulesVitest, domain/tests/ (cd domain && npm test)Pure logic: obligation rules, form bundles, XRF, licensing, ingestion normalizers and runners against an in-memory Db
Legacy fidelitydomain/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 helpersVitest, src/**/*.test.ts and pure supabase/functions/** modules (npm run test)UI logic, URL codecs, report HTML builders, retry math
Edge-function typesDeno, npm run check:functionsType-correctness of every function and the domain code it imports
DatabaseVitest, db-tests/ against local PostgresCross-tenant leakage matrix, permission matrix and policy lint, ingestion constraints, security fixes
Map serviceVitest, 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.ts throws at import time without VITE_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.local aside 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-tests inserts fixture rows into auth.users on 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/ and db-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):

LabelMeaning
needs-triageA maintainer needs to evaluate it
needs-infoWaiting on the reporter
ready-for-agentFully specified; an unattended agent can take it
ready-for-humanNeeds human implementation
wontfixWill 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.