diff --git a/.fallowrc.json b/.fallowrc.json index 57f486d..f587e92 100644 --- a/.fallowrc.json +++ b/.fallowrc.json @@ -22,6 +22,23 @@ "zod" ], "rules": { + "boundary-violation": "error", "unused-dependencies": "warn" + }, + "boundaries": { + "zones": [ + { "name": "domain", "patterns": ["src/domain/**"] }, + { "name": "infra", "patterns": ["src/infra/**"] }, + { "name": "api", "patterns": ["src/api/**"] }, + { "name": "webapp", "patterns": ["src/webapp/**"] }, + { "name": "server", "patterns": ["src/server.ts"] } + ], + "rules": [ + { "from": "domain", "allow": [] }, + { "from": "infra", "allow": ["domain"] }, + { "from": "api", "allow": ["domain", "infra"] }, + { "from": "webapp", "allow": ["domain", "api"] }, + { "from": "server", "allow": ["domain", "infra", "api", "webapp"] } + ] } } diff --git a/AGENTS.md b/AGENTS.md index aaf261f..cf25f58 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -30,7 +30,7 @@ This project uses **Vite 8**, **Vitest 4**, **Stryker**, **oxlint**, **oxfmt**, - `pnpm lint` - Lint and auto-fix with oxlint (includes local `starter/*` JS plugin rules in `tools/oxlint-plugins/`) - `pnpm format` - Format code with oxfmt -- `pnpm run ci` - Full CI check: lint, format, and typecheck +- `pnpm run ci` - Full CI check: lint, format, typecheck, and Fallow audit ### Dependencies @@ -46,6 +46,7 @@ This project uses **Vite 8**, **Vitest 4**, **Stryker**, **oxlint**, **oxfmt**, - **Stryker config**: `stryker.config.mjs` (mutation scope, reporters, 80% break threshold) - **oxlint config**: `oxlint.config.ts` (type-aware linting, ignore patterns) - **oxfmt config**: `oxfmt.config.ts` (formatting: no semi, single quotes) +- **Fallow config**: `.fallowrc.json` (dead code analysis, dependency checks, custom architecture boundary zones) - **TypeScript**: `tsconfig.json` and layer-specific configs ## Imports @@ -60,7 +61,7 @@ This project uses **Vite 8**, **Vitest 4**, **Stryker**, **oxlint**, **oxfmt**, Git hooks are managed by [lefthook](https://lefthook.dev), configured in `lefthook.yml`. -- **pre-commit** runs `oxlint --fix` and `oxfmt --write` on staged files (auto-restaged), then runs `vitest related` over the staged files — only unit tests that import a staged file execute. +- **pre-commit** runs `oxlint --fix` and `oxfmt --write` on staged files (auto-restaged), then runs `vitest related` over the staged files — only unit tests that import a staged file execute. It also runs `pnpm fallow:ci` to enforce Fallow checks before each commit. - **pre-push** runs `vitest run --changed origin/master --project unit`, executing only the unit tests affected by files changed against `origin/master`. Mutation testing is intentionally not a git hook because it is slower than the commit/push feedback loop. Run `pnpm test:mutate` before merging changes to `src/domain/**`, `src/api/**`, or unit-test behaviour; CI enforces the same command for those paths. @@ -71,4 +72,5 @@ Hooks install automatically via the `prepare` script (`lefthook install`). To sk - [ ] Run `pnpm install` after pulling remote changes and before getting started. - [ ] Run `pnpm run ci` and `pnpm test` to validate changes. +- [ ] Run `pnpm fallow list --boundaries` and `pnpm fallow dead-code --boundary-violations` after changing architecture boundary config. - [ ] Run `pnpm test:mutate` when changing mutation-scoped logic or unit tests. diff --git a/README.md b/README.md index 202d4d8..00e80dc 100644 --- a/README.md +++ b/README.md @@ -53,13 +53,14 @@ Business logic lives in pure TypeScript. Frameworks, databases, and external ser - `src/api/` — HTTP composition root (Hono). Wires concrete adapters into use cases and exposes them as routes. - `src/webapp/` — React + TanStack Start UI. Calls into the API; composes its own adapters where needed. -**The boundary is enforced three times, on purpose:** +**The boundary is enforced four times, on purpose:** 1. **Path aliases** — `@domain/*` and `@infra/*` in `tsconfig.json` make the layer of every import readable at a glance. 2. **Layered typechecks** — `tsconfig.domain.json` compiles the domain alone; `tsconfig.infra.json` compiles domain + infra. If domain code reaches into infra, the domain typecheck fails before anything else runs. `pnpm typecheck:layers` runs both. 3. **Custom oxlint rule** — `starter/domain-no-infra-imports` (in `tools/oxlint-plugins/rules/`) blocks both static and dynamic imports from `src/infra/**` inside `src/domain/**` files. +4. **Fallow custom boundaries** — `.fallowrc.json` defines explicit zones for `domain`, `infra`, `api`, `webapp`, and `server`, then fails `boundary-violation` issues in CI. -Any one of the three would catch most mistakes; all three together make the violation loud and local. +Any one of the four would catch most mistakes; all four together make the violation loud and local. ## Directory layout @@ -147,7 +148,7 @@ Rule of thumb: if a module contains only declarations, use `*.schema.ts` or `*.t Every stage has a specific job. Understanding the _why_ matters as much as the commands. -- **Pre-commit (lefthook)** — runs `oxlint --fix` and `oxfmt --write` on staged files (auto-restaged), then `vitest related --run --project unit` over the staged files. _Why_: keeps git history clean and readable (no "fix lint" commits), and ensures every commit is **independently releasable** — no commit silently breaks the behaviour of code near the change. +- **Pre-commit (lefthook)** — runs `oxlint --fix` and `oxfmt --write` on staged files (auto-restaged), then `vitest related --run --project unit` over the staged files and `pnpm fallow:ci` for Fallow checks. _Why_: keeps git history clean and readable (no "fix lint" commits), and ensures every commit is **independently releasable** — no commit silently breaks the behaviour or architecture of code near the change. - **Pre-push (lefthook)** — `vitest run --changed origin/master --project unit`. Catches regressions across the whole change set before they leave the machine. - **`pnpm run ci`** (local + CI) — `oxlint && oxfmt --check . && pnpm typecheck:layers && pnpm fallow:ci`. Read-only quality gate; no fixes, no writes. The source of truth for "is this branch green?" - **GitHub Actions** — runs the same gate plus the test matrix (unit, browser, integration). @@ -178,10 +179,12 @@ pnpm fallow # full analysis (dead code + dupes + health) pnpm fallow dead-code # unused code + circular deps only pnpm fallow dupes # duplication scan pnpm fallow health # complexity + maintainability +pnpm fallow list --boundaries +pnpm fallow dead-code --boundary-violations pnpm fallow fix --dry-run # preview auto-fixes for unused exports/deps ``` -`pnpm run ci` runs `pnpm fallow:ci` (`fallow audit`) as a quality gate — it scopes analysis to files changed against the base branch and returns a pass/warn/fail verdict. Configuration lives in `.fallowrc.json`. For the full feature set, see the [fallow docs](https://docs.fallow.tools). +`pnpm run ci` runs `pnpm fallow:ci` (`fallow audit`) as a quality gate — it scopes analysis to files changed against the base branch and returns a pass/warn/fail verdict. Configuration lives in `.fallowrc.json`, including custom boundary zones and rules for the hexagonal architecture. For the full feature set, see the [fallow docs](https://docs.fallow.tools). ## Reproducing this setup in your own repo @@ -191,7 +194,7 @@ A short checklist for applying these conventions to a greenfield project: 2. Create `src/domain`, `src/infra`, `src/utils` on day one, even if empty. Directory shape is a commitment. 3. Copy the `tsconfig.json` + `tsconfig.domain.json` + `tsconfig.infra.json` trio; adjust the includes/aliases. 4. Set up `oxlint.config.ts` and `oxfmt.config.ts`. Copy `tools/oxlint-plugins/` and rename the `starter` namespace. -5. Add `lefthook.yml` with pre-commit (lint + format + `vitest related --project unit`) and pre-push (`vitest run --changed origin/master --project unit`). +5. Add `lefthook.yml` with pre-commit (lint + format + `vitest related --project unit` + `pnpm fallow:ci`) and pre-push (`vitest run --changed origin/master --project unit`). 6. Add the `pnpm ci` script: lint → format check → layered typecheck → fallow audit. 7. Add Stryker (`@stryker-mutator/core`, `@stryker-mutator/vitest-runner`), `stryker.config.mjs`, `vitest.mutation.config.ts`, and the `pnpm test:mutate` script with an 80% `thresholds.break` floor. 8. Adopt the four test-type filename suffixes (`.unit`, `.browser`, `.integration`, `.e2e`) before writing any tests. diff --git a/lefthook.yml b/lefthook.yml index 740d903..67bcfa8 100644 --- a/lefthook.yml +++ b/lefthook.yml @@ -10,6 +10,8 @@ pre-commit: - name: vitest glob: '*.{js,ts,tsx,jsx,mjs,cjs}' run: pnpm vitest related --run --project unit --passWithNoTests {staged_files} + - name: fallow + run: pnpm fallow:ci pre-push: jobs: