> ## Documentation Index
> Fetch the complete documentation index at: https://docs.soundchecklive.io/llms.txt
> Use this file to discover all available pages before exploring further.

# Code Quality

> TypeScript configuration and development standards

This document outlines the development workflow and code quality standards for Soundcheck.

## TypeScript configuration

### Strict mode enabled

The project uses strict TypeScript configuration:

| Setting | Enabled | Purpose |
| - | - | - |
| Strict Mode | ✅ | All strict checks enabled |
| No Implicit Any | ✅ | Variables must have explicit types |
| Strict Null Checks | ✅ | Prevents null/undefined errors |
| No Unused Variables | ✅ | Catches unused imports |
| No Unchecked Indexed Access | ✅ | Safer array/object access |

### Benefits

* **Prevents Production Crashes** - Catches type mismatches before deployment
* **Better IntelliSense** - More accurate code completion
* **Safer Refactoring** - TypeScript catches breaking changes

***

## Development scripts

### Daily development

```bash theme={null}
# Start development server with type checking
npm run dev

# Type check only (no build)
npm run type-check

# Watch mode type checking
npm run type-check:watch

# Lint code
npm run lint

# Auto-fix linting issues
npm run lint:fix

# Format code
npm run format

# Check formatting
npm run format:check

# Run all checks (type + lint + format)
npm run check-all
```

### Pre-build checks

```bash theme={null}
# Runs automatically before builds
npm run prebuild

# Manual check before committing
npm run check-all
```

***

## Git hooks

Husky installs two hooks when you run `npm ci` at the repo root. Both run only the checks for the packages you touched. Both print their slowest steps when they take 10 seconds or more (set `SOUNDCHECK_HOOK_TIMINGS_MIN` to change the limit).

### Pre-commit (`.husky/pre-commit`)

Pre-commit works on staged files only:

1. **Fast gates**: the comment ban, analytics taxonomy, feature flag registry, and the node:test suites beside any staged repo script. The `gates/registry.json` script gates only read the tree, so they run in parallel and print their output in table order (`SOUNDCHECK_HOOK_JOBS=1` runs them one at a time).
2. **Lint and format**: oxlint on each package's staged files. oxfmt checks each package's staged files in one run and restages what it formats. Mobile files go through `lint-staged`, which covers the paths the root `lint` and `format` scripts cover.
3. **Related tests** for web, api-events, and mcp-ts. `vitest run` runs the tests that import the staged files, nearest first and at most 40 per package, without coverage. The packages run in parallel after every package's lint and format. Then `scripts/check-weak-tests.mjs --staged` prints a `WEAK` line for each new test case whose only expectations cannot fail for a defect (`toBeDefined`, `toHaveBeenCalled`, `not.toThrow`, and similar). It is advisory and never blocks the commit.
4. **Scanners and file linters**: SQL timezone and LIKE escape, Prisma validate, whitespace, shellcheck, actionlint, Markdown tables, and docs links.

Coverage is not a gate. No hook or CI check fails on a coverage percentage; `npm run test:coverage` in a package still writes a report when you want one. Write tests for the behavior you change, not to reach a number.

`scripts/related-test-targets.mjs` decides which tests run. It walks the package's import graph itself (`scripts/related-test-graph.mjs`, about a second) instead of calling `vitest related`, which transforms every test file's imports before it starts. For a widely imported helper that took two minutes or more. Type-only imports do not count. When more than 40 tests import the staged files, the hook runs the 40 closest: fewest import hops first, then the nearest folders. It prints how many it left to CI, which runs every suite on the PR. `SOUNDCHECK_PRECOMMIT_TEST_CAP=<n>` changes the cap (`0` runs them all).

It starts from these files:

* A staged source file is passed as-is.
* A data file, such as `migration.sql`, `schema.prisma`, JSON, a fixture, or a snapshot, becomes the source files that name it or quote its folder.
* A deleted module becomes the files that import it.
* These need the **full suite**, which pre-commit leaves to CI: `package.json`, a lockfile, a tsconfig, a vitest config, a loaded `.env` file, `src/test/`, `__mocks__/`, or a nested data file that no source names. The hook prints the reason and runs no tests for that package. `SOUNDCHECK_PRECOMMIT_FULL_TESTS=1` runs the suite locally instead.

During a merge, a file counts only when the merge result differs from both parents, so merging `dev` does not re-test everything `dev` changed.

### Pre-push (`.husky/pre-push`)

Pre-push plans the pushed range with `scripts/affected/affected.mjs`, then runs whole-package type checks, lint, Knip, and the SQL scanners for the affected packages. It installs and runs code generation first, then runs every check as its own parallel job, so the push takes about as long as its slowest check. After those pass, it measures coverage for unit-gated source in the push (see above). When the plan asks for a full unit suite, for example because a lockfile, auth, or money path changed, pre-push leaves that suite to CI, which runs each affected package's full suite on the PR. E2E, integration, and OpenAPI parity also run in CI.

### Parallel jobs

Both hooks run their slow checks as background jobs and print each job's output in order once it finishes. Code generation runs once before the jobs start, and the jobs call npm with `--ignore-scripts`. Otherwise api-events and checkai would both rebuild mcp-ts, and web would regenerate the client mobile reads, at the same time. Set `SOUNDCHECK_HOOK_JOBS=1` to run the jobs one at a time, for example on a laptop with little memory.

Only a few hooks per clone run their jobs at a time: one per 18 GB of memory, at least one (two on a 36 GB Mac). On Linux the count uses the lowest cgroup memory limit on the hook's own cgroup or any parent (a container or systemd slice limit) when that is lower than the machine's memory. Parallel agent sessions each commit from their own worktree, and a single api-events typecheck takes about 7 GB, so several hooks at once can fill memory and swap. A hook that finds every slot busy prints which worktrees hold them and its place in line, and waits. Waiting hooks get a slot in the order they arrived. Pre-commit test runs and pre-push type checks, full suites, and knip take a slot; a push that only runs lint or the SQL scanners does not wait. A slot is a kernel file lock (taken through perl), so a hook that dies frees its slot at once; after 30 minutes a waiting hook stops waiting and runs anyway. Without perl the hooks run without the limit. `SOUNDCHECK_HOOK_SLOTS=<n>` lets n hooks run at once (`0` turns the wait off), and `SOUNDCHECK_HOOK_SLOT_WAIT=<seconds>` changes the 30-minute limit. Vitest runs in the hooks use at most 4 workers; set `SOUNDCHECK_VITEST_MAX_WORKERS` to change that.

The heavy package scripts wait for the same slots outside the hooks. They run through `scripts/check-slot.mjs`, which takes a slot for the length of the command:

| Package | Scripts that wait for a slot |
| - | - |
| root (mobile) | `type-check`, `type-check:tests`, `test:unit` |
| `web/` | `typecheck`, `test`, `test:coverage` |
| `api-events/` | `typecheck`, `test`, `test:coverage` |
| `mcp-ts/` | `typecheck`, `test`, `test:coverage` |
| `checkai/` | `typecheck`, `test` |
| `e2e/` | `lint:tsc`, `test:unit` (not `test`, which drives a running stack) |

Scripts that only chain these (`checks`, `check-all`, `type-check:all`) wait once for each wrapped step. An agent that runs `npm run typecheck` therefore queues behind other worktrees' hooks and type checks instead of running alongside them. A hook that already holds a slot exports `SOUNDCHECK_HOOK_SLOT_HELD=1`, so the package scripts it runs do not wait for a second slot. The wrapper does not queue in CI, on Windows, without perl, or with `SOUNDCHECK_HOOK_SLOTS=0`. A direct `npx tsc` or `npx vitest` skips the queue, so run checks through the package scripts.

### Measured times

Measured on an Apple Silicon laptop with warm caches, on 2026-09-29:

| Change | Hook | Before ENG-1040 | After |
| - | - | - | - |
| Feature: 17 web and 11 api-events files | pre-commit | 69s | 59s |
| Feature: 17 web and 11 api-events files | pre-push | 123s | 38s |
| Schema change: a migration and 10 api-events files | pre-commit | 78s (171s cold) | 47s (54s cold) |
| Merge of 297 `dev` commits (1,146 files) | pre-commit | 332s | not measured again |

Parallel jobs, measured the same way against the serial hooks above:

| Change | Hook | Serial | Parallel |
| - | - | - | - |
| Feature: 17 web and 11 api-events files | pre-commit | 56s | 41s |
| Push that affects every package | pre-push | 44s | 23s |

In the merge, 197 of the 332 seconds went to formatting one file at a time, which the batched oxfmt run replaces. Related tests are now the largest step in a normal commit, followed by web and api-events type checks at pre-push.

The target is under 2 minutes for a typical commit and under 5 to 8 minutes for a typical push. These cases still take longer:

* **A cold install.** The first commit or push after a lockfile change runs `npm ci` in each touched package (about 20 seconds to 2 minutes).
* **A staged `package.json`.** Pre-commit syncs the lockfile with `npm install`, which also reruns code generation (5 to 35 seconds).
* **Changes in several packages** run each package's steps in turn.

### When a hook is slow

* Read the timing summary at the end of the hook output. It names the slow step.
* Commit changes to web, api-events, and mcp-ts separately when they do not depend on each other.
* Keep `node_modules` real and current. In a worktree, run `npm run worktree:setup` once, not a symlink.
* `SOUNDCHECK_PRECOMMIT_FULL_TESTS=1` forces the full suite at pre-commit. `SOUNDCHECK_PREPUSH_FULL=1` runs the full unit suites at pre-push. Use them when you want local proof before CI.
* Do not use `git commit --no-verify` or `git push --no-verify`. The hooks run the same gates as CI, so a bypass moves the failure to the PR.

***

## Dependency cooling (supply chain)

New dependency versions wait before install/build so automated scanners have time to catch compromised publishes.

| Ecosystem | Mechanism | Window | Where |
| - | - | - | - |
| npm (root, `web/`, `api-events/`, `e2e/`, `checkai/`, `docs/`) | `.npmrc` `min-release-age=2` (requires npm ≥ 11.10; repo pins 11.15.0) | 48 hours | Install time |
| Go / Actions / Terraform | Renovate `minimumReleaseAge` (global default 1 day; package rules may tighten) | ≥ 1 day | PR generation |
| Cargo (`infrastructure-services/gcp-platform-signals/`) | `build.rustc-wrapper` + Cursor hook + CI script + Renovate cargo rule | 48 hours | Build / PR / Renovate |

Stable Cargo has no install-time equivalent (RFC 3923 `min-publish-age` is nightly-only). Compiling a crate runs its `build.rs` and proc macros, so the Rust module uses an editor-agnostic `rustc-wrapper` (`infrastructure-services/gcp-platform-signals/.cargo/rustc-guard.mjs`) plus CI (`scripts/check-cargo-release-age.mjs`). Ops detail and emergency bypass: `AGENTS.md`. Recurring trap: [Pitfalls §13](https://docs.soundchecklive.io/engineering/reference/pitfalls.md#13-fresh-rust-crates-execute-at-compile-time).

One-off npm bypass (emergency only):

```bash theme={null}
npm install <pkg>@<version> --config-min-release-age=0
```

***

## oxlint rules

### TypeScript rules

| Rule | Severity | Purpose |
| - | - | - |
| `no-unused-vars` | Warning | Remove unused variables |
| `no-explicit-any` | Warning | Replace `any` with specific types |
| `no-non-null-assertion` | Warning | Avoid `!` assertions |

### React rules

| Rule | Severity | Purpose |
| - | - | - |
| `rules-of-hooks` | Error | Hooks must follow React rules |
| `exhaustive-deps` | Warning | Include all useEffect dependencies |

### General quality

| Rule | Severity | Purpose |
| - | - | - |
| `prefer-const` | Warning | Use const when variables don't change |
| `no-var` | Error | Use let/const instead of var |
| `no-debugger` | Error | Remove debugger statements |

***

## Common issues and fixes

> [accordion] Element type is invalid
>
> **Cause**: Import/export mismatch between components and contexts
>
> **Fix**: Ensure all interfaces are imported from the same source:
>
> ```tsx theme={null}
> // ❌ Wrong - conflicting interfaces
> interface Event { id: string }  // In one file
> interface Event { id: number }  // In another file
>
> // ✅ Correct - single source of truth
> import { Event } from '@/lib/types/events';
> ```

> [accordion] Rules of Hooks Violations
>
> **Cause**: Calling hooks conditionally
>
> ```tsx theme={null}
> // ❌ Wrong
> if (condition) {
>   const data = useAuth();
> }
>
> // ✅ Correct
> const data = useAuth();
> if (condition && data) {
>   // use data
> }
> ```

> [accordion] Missing Dependencies in useEffect
>
> **Fix**: Include all dependencies or disable the rule:
>
> ```tsx theme={null}
> // ✅ Include dependency
> useEffect(() => {
>   fetchData();
> }, [fetchData]);
>
> // ✅ Or disable if intentional
> useEffect(() => {
>   fetchData();
>   // eslint-disable-next-line react-hooks/exhaustive-deps
> }, []);
> ```

***

## Testing environments

### Development vs production

| Environment | Behavior |
| - | - |
| Development | More forgiving, better debugging |
| Production | Strict type checking, optimized |
| TestFlight | Production-like with crash reporting |

### Recommended testing flow

1. **Local Development**: `npm run dev`
2. **Pre-commit**: Automatic checks via git hooks
3. **Pre-build**: `npm run check-all`
4. **Local Production Build**: `npx eas build --local`
5. **TestFlight**: Deploy and test

***

## Migration strategy

### Current state

* **TypeScript**: Strict mode enabled
* **oxlint**: Warnings for most rules (not blocking)
* **oxfmt**: Formatting enforced by `npm run format:check`
* **Git Hooks**: Enabled but permissive

### Future improvements

1. **Phase 1**: Fix critical warnings (unused variables, React hooks)
2. **Phase 2**: Replace `any` types with specific interfaces
3. **Phase 3**: Enable stricter oxlint rules as errors
4. **Phase 4**: Add unit tests with type checking

***

## VS Code integration

### Recommended extensions

* TypeScript and JavaScript Language Features
* Oxc / oxlint
* Expo Tools

### Auto-configuration

The `.vscode/` folder contains:

* **settings.json**: Auto-format on save, lint integration
* **extensions.json**: Recommended extensions for the team

***

## Goals

### Short term

* Prevent type-related crashes in production
* Catch import/export issues before deployment
* Enforce React best practices

### Long term

* Achieve 100% TypeScript coverage
* Zero oxlint warnings
* Comprehensive test coverage with type safety

***

## Need help?

1. **Check this document** for common solutions
2. **Run `npm run check-all`** to see all issues
3. **Use `npm run lint:fix`** to auto-fix simple issues
4. **Temporarily disable rules** with lint comments if needed
5. **Ask the team** for help with complex type issues

> [!NOTE]
>
> These strict checks prevent bugs that would otherwise crash the app in production.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.