# Constraints Enforced quality/security bar for this repo. Agents must not weaken any rule here to make a change pass — fix the change instead. If a rule is genuinely wrong, edit this file in its own commit and say why. Enforcement mode: **block**. A failing check stops the task; fix it before continuing. ## Floor (existing, unchanged) | Check | Command | Rule | |---|---|---| | Types | `pnpm typecheck` | 0 errors across main/preload/renderer tsconfigs | | Lint | `pnpm lint` | `eslint --max-warnings 0` | | Format | `pnpm format:check` | Prettier clean | | Commit messages | `pnpm lint:commits` (CI checks each PR's range) | Conventional Commits, header ≤ 72 characters (`commitlint.config.ts`) | ## Code comments | Check | Command | Rule | Reason | |---|---|---|---| | Comment intent | `pnpm lint` | 0 `local/no-history-comments` errors (`eslint-rules/no-history-comments.mjs`) | Comments explain *why*. Issue/PR tags, task numbers, tooling narrative and `file:line` refs belong in the commit or PR, and go stale in the code. Narrative and duplication aren't machine-checkable — see CLAUDE.md, Code Style. | ## Security | Check | Command | Rule | Reason | |---|---|---|---| | Vault/IPC boundary | `pnpm depcruise` | 0 violations of `renderer-no-vault-crypto` (`.dependency-cruiser.cjs`) | Codifies the CLAUDE.md rule: renderer must never import `src/main/vault/**`, `src/main/db/**`, `@journeyapps/sqlcipher`, or `libsodium-wrappers-sumo` directly — everything crosses the IPC boundary. | | Sandboxed preload | `pnpm depcruise` | 0 violations of `preload-sandbox-compatible` (`.dependency-cruiser.cjs`) | The renderer runs sandboxed, so `src/preload/**` may only import `electron` at runtime. A Node builtin or any other package builds fine but fails at runtime, leaving `window.notvex` undefined. | | Error log allowlist | `pnpm test:app` | `tests/main/app/error-log.test.ts` green; every write to `logs/notvex.log` goes through `formatEntry` in `src/main/error-log.ts` | The log is plaintext and readable without the vault password, so it may hold only what is already as exposed as `prefs.json`: never `error.message`, `cause`, IPC arguments, vault or user file paths, or note/tag data. Messages embed input (V8's `JSON.parse` errors quote the parsed text, fs errors carry the vault path), so entries are built from an allowlist (error name, stack frames trimmed to the app bundle, a fixed reason) rather than redacted. The stack's `Name: message` header is cut by the message's exact text (`src/shared/error-stack.ts`) before any line is parsed, since a multi-line message can contain frame-shaped lines. The sentinel tests in that file fail if anything outside the allowlist reaches the output. The file is `0o600` in a `0o700` folder on macOS/Linux; Windows ignores those modes, and the per-user ACL on `%APPDATA%` is what protects it there. | | Secrets scan | *(pending, see Exceptions)* | 0 findings, `gitleaks detect --redact` | Vault key material / recovery phrases must never land in a commit. | | Dependency vulns | *(pending, see Exceptions)* | 0 high/critical, `osv-scanner scan .` | `@journeyapps/sqlcipher`, `libsodium-wrappers-sumo`, `koffi` are native-binding deps in the crypto path — a known CVE there is high-severity for this app specifically. | ## Architecture Same `pnpm depcruise` run as above — one dependency-cruiser config, two concerns (boundary correctness doubles as the architecture check for now). Add a second rule to `.dependency-cruiser.cjs` if another boundary needs enforcing later; don't add a second tool for it. ## Test file location Test files live under `tests/` at the project root, never colocated inside `src/`. `pnpm lint`/`pnpm format`/`typecheck` all cover `tests/**` explicitly — a test file outside that scope is silently unchecked by all three. Two typecheck scopes, because React tests need `jsx` and DOM libs that the main-process config must not carry: | Location | tsconfig | Environment | |---|---|---| | `tests/main/**`, `tests/preload/**`, `tests/eslint-rules/**` | `tsconfig.node.json` | node (default) | | `tests/renderer/*.test.ts` | `tsconfig.web.json` | jsdom, via a `// @vitest-environment jsdom` docblock | `tsconfig.node.json` excludes `tests/renderer`; `tsconfig.web.json` includes it. A React test placed outside `tests/renderer/` will fail to typecheck. Folders mirror the code they test (`tests/main/vault/` tests `src/main/vault/`), and each `pnpm test:` script runs one of them. Split a file by topic once it grows past a few hundred lines. Fixtures shared by several files live next to them in a non-`.test.ts` module (`tests/main/vault/helpers.ts`). `vi.mock` is hoisted only within its own file. A shared module can hold the mocks (`tests/main/ipc/confirmation-gates/harness.ts`) only when the test files import the mocked code dynamically, after the harness has loaded; with static imports, keep the `vi.mock` calls in each test file. Tests import source through the path aliases (`@main/…`, `@/…`), in `import`, `vi.mock` and `typeof import()` alike, so a file can move without its paths changing. Vitest resolves a mocked alias to the same module as the source's relative import of it. React tests that depend on effects re-running must pass `reactStrictMode: true` to `render`/`renderHook`. Wrapping the tree in `` by hand does **not** re-run effects under Testing Library, so such a test silently proves nothing — this was verified with a probe, not assumed. ## Coverage — measured only, not gated Measured via `pnpm test:coverage`: | Metric | Value | Previous floor | |---|---|---| | Statements | 55.99% (2387/4263) | 51.72% (2161/4178) | | Branches | 48.63% (1019/2095) | 43.07% (887/2059) | | Functions | 48.87% (521/1066) | 44.96% (469/1043) | | Lines | 57.38% (2183/3804) | 53.13% (1982/3730) | Coverage percentage alone does not prove a test asserts anything: a covered line can still pass against deliberately broken code. - Floor: the numbers above. From here, coverage must not regress below this floor — re-run `pnpm test:coverage` and update this table when it improves. - Do not invent a target (e.g. "80%") — that's fabricated, not measured. A percentage can also fall because the denominator shrank: less code under the same tests (e.g. statements 666/3391 → 665/3390 and functions 131/852 → 129/850 after a simplification pass, same 37 tests). That direction is fine when it is recorded here with its before/after counts, because the rule above exists to stop a number being quietly edited down. A drop with no such note, or one where the test count also fell, is the thing it is guarding against. ## Speed budget | Tier | Commands | Target | Measured | |---|---|---|---| | Fast (edit loop) | `pnpm lint` (eslint `--cache`) | seconds | ~30s cold, faster warm | | Task-end | `pnpm check:task` (= validate + depcruise + lint:commits + test) | 90s | **~2m55s — over budget** | | Full (review/CI) | `pnpm check:full` (= check:task + coverage) | unlimited | not yet run in CI | The 90s task-end target isn't met: `pnpm typecheck` alone measures **~2m13s** because it runs three separate `tsc --noEmit` invocations (`tsconfig.json` + node + web) with `--composite false`, which disables incremental caching on every run. That's a pre-existing property of the tsconfig split, not something this pass changed — flagging it here rather than restructuring the TS project setup as an unscoped side quest. If task-end speed becomes a real problem, the fix is enabling incremental `tsc` builds (cache the `.tsbuildinfo` files instead of `--composite false`), not dropping typecheck from the gate. Until then: run `pnpm lint` (and `pnpm depcruise`, ~11s) during a task; save `pnpm check:task` for before marking work done, same as `pnpm validate` was already used per CLAUDE.md. ## Scripts ```bash pnpm depcruise # architecture/IPC-boundary check (.dependency-cruiser.cjs) pnpm check:fast # typecheck + lint — edit loop pnpm check:task # validate + depcruise + lint:commits + test — before calling work done pnpm check:full # check:task + coverage — review/CI pnpm test # vitest run — every test pnpm test:watch # vitest in watch mode pnpm test:main # tests/main (all main-process tests) pnpm test:vault # tests/main/vault pnpm test:db # tests/main/db pnpm test:ipc # tests/main/ipc pnpm test:app # tests/main/app (lifecycle, window, updater, guards) pnpm test:preload # tests/preload pnpm test:renderer # tests/renderer (jsdom) pnpm test:coverage # vitest run --coverage ``` ## Exceptions | Rule | Status | Owner | Expiry | Note | |---|---|---|---|---| | Secrets scan (gitleaks) | Not installed | GFrancV | 2026-10-07 | Not on npm; real binary is `winget install Gitleaks.Gitleaks`. Skipped installing system-wide during setup per explicit request — install manually, then wire `gitleaks detect --redact --source .` into `check:full`. | | Dependency vuln scan (osv-scanner) | Not installed | GFrancV | 2026-10-07 | Not on npm; real binary is `winget install Google.OSVScanner`. Same as above — install manually, then wire `osv-scanner scan --lockfile pnpm-lock.yaml` into `check:full`. | | `check:task` speed budget | Over 90s target (~2m55s) | GFrancV | — | See Speed budget above. No expiry: this is a measured fact about the current tsconfig setup, not a temporary exception someone forgot to clean up. Revisit only if incremental `tsc` is enabled. | Review the two "not installed" exceptions by the expiry date above — either install the tools and wire them in, or renew the exception with a reason.