Architecture Checks in CI
Section titled “Architecture Checks in CI”This document explains what architecture checks run in CI, how to run them locally, and what to do when they fail.
Quick start
Section titled “Quick start”# Run import-boundary lint on all boundary-critical pathspnpm lint:arch
# Run structural hygiene checks (tracked artifacts, required docs, etc.)pnpm check:arch
# Detect workspace package dependency cyclespnpm check:cycles
# Run all three (what CI runs in the architecture-checks job)pnpm ci:archBoth commands are included in the local pnpm validate sequence, so running
pnpm validate before pushing is sufficient for full coverage.
If you are not sure where new code should go before running checks, start with 16-contributing-and-templates.md.
What runs in CI
Section titled “What runs in CI”Architecture checks run in a dedicated architecture-checks CI job in
.github/workflows/turbo-ci.yml. The job is separate from the main validate
job so failures are easy to identify and act on.
Step 1: pnpm lint:arch — import-boundary lint
Section titled “Step 1: pnpm lint:arch — import-boundary lint”Runs Oxlint (same config as pnpm lint) scoped specifically to the paths where
architectural boundary rules are enforced:
| Path | Boundary rules enforced |
|---|---|
packages/feature-*/src | Must not import apps, @/, server fns, DB directly |
packages/platform-*/src | Must not import feature packages or app code |
packages/domain-*/src | Must not import React, TanStack UI, or CF runtime |
packages/db/src | Must not import React, TanStack UI, or app code |
packages/utils/src | Must not import React or feature packages |
packages/types/src | Must not import app code or aliases |
packages/schemas/src | Must not import app code or aliases |
packages/api-contract/src | Must not import app code or aliases |
packages/integration-core/src | Must not import app code or aliases |
apps/dashboard/src/features | Must not statically import server fns; use api.ts |
apps/dashboard/src/components | Must not import feature modules |
apps/dashboard/src/shared | Must not import CF runtime or server-only modules |
apps/dashboard/src/lib | Must not import feature modules |
apps/dashboard/src/app | Must not import server fns or feature internals |
apps/dashboard/src/routes | Must not import feature internal subfolders |
The full rule set is in .oxlintrc.json. Canonical
dependency direction rules: docs/architecture/03-dependency-rules.md.
When it fails: Check the oxlint error message. It will name the violated rule and include a message describing the correct pattern (e.g. “use api.ts wrapper with dynamic import() + call()”).
Barrel rule enforcement for cross-feature internal imports is included. Routes
and app/ must consume feature modules through their public boundary
(@/features/{name}) and may not deep-import into internal subfolders
(components/, hooks/, utils/, lib/, store/, mutations/, model/).
See 14-barrel-rules.md — Rule 1 and Rule 3.
Step 2: pnpm check:arch — structural hygiene checks
Section titled “Step 2: pnpm check:arch — structural hygiene checks”Runs scripts/check-arch.mjs. Each check is fast, git-based, or file-system
based. No build required.
| Check | Enforcement | Notes |
|---|---|---|
| No artifact directories tracked in git | Blocking | Catches coverage/, dist/, logs/, .wrangler/, test-results/ accidentally committed. See 12-generated-artifacts.md. |
No stray root-level src/ directory | Blocking | Guards against code added at repo root. All code belongs in apps/ or packages/. |
| Required architecture docs present | Blocking | Key architecture docs (00–03, 12, 13) must not be deleted. |
| Intentionally committed generated artifacts present | Blocking | Allowlisted files (routeTree.gen.ts, events/emitters.ts, event-catalog.md) must be regenerated if missing. |
Deprecated workers/my-api/src | Staged (non-blocking) | Prints a warning if present; does not fail CI. Follow-up: monitor for growth; full removal tracked in 06-current-gaps.md. |
| Split ingestion topology present | Blocking | Requires workers/ingestion-orchestrator, workers/ingestion-process, and packages/platform-ingestion; fails if the retired legacy ingestion package returns. |
| No retired legacy-ingestion refs in active docs | Blocking | Prevents current scaffolding, repo map, and onboarding docs from drifting back to the removed legacy ingestion package. |
Enforcement levels
Section titled “Enforcement levels”| Check | Level | Why |
|---|---|---|
Import-boundary lint (lint:arch) | Blocking | Rules are mature; boundary violations are clear regressions. |
| Tracked-artifact hygiene | Blocking | Simple and stable; git ls-files is reliable. |
| Required architecture docs | Blocking | Docs must not vanish silently. |
| Allowlisted generated artifacts | Blocking | Missing files indicate a broken generation workflow. |
worker-my-api deprecation | Staged (warn only) | Still present for reference; not yet safe to enforce removal. |
| Split ingestion topology hygiene | Blocking | The split worker + shared runtime layout is now canonical. |
| Retired legacy-ingestion references | Blocking | Active guidance must keep pointing at workers/ingestion-* and @platform/ingestion. |
Cycle detection (check:cycles) | Blocking | Pure-Node DFS over workspace package.json dep graph; zero external deps. |
| Workspace dependency direction (graph) | Not yet added | depcruise config needed. See §Future checks below. |
What to do when a check fails
Section titled “What to do when a check fails”Import-boundary lint failure
Section titled “Import-boundary lint failure”Read the Oxlint message carefully. Each boundary rule includes a human-readable message pointing to the correct pattern:
Feature packages must not statically import server function modules.Use dynamic import() + call() from api.ts(see docs/architecture/03-dependency-rules.md)Fix the import using the pattern described. Common cases:
- Feature importing server function → add a wrapper in the feature’s
api.tsusingdynamic import() + call() - Shared component importing feature → move shared logic to a utility or keep it inside the feature
- Package importing app code → use a package entrypoint or
@host/*shim
Structural hygiene failure
Section titled “Structural hygiene failure”Each check in check:arch prints a → hint with the remediation step:
❌ 2 artifact file(s) tracked in git → Run: git rm --cached <file> See: docs/architecture/12-generated-artifacts.mdFollow the hint. If you believe the file should be committed, check 12-generated-artifacts.md for the allowlist policy and add an explicit exception there if justified.
Cycle detection failure
Section titled “Cycle detection failure”pnpm check:cycles reads all workspace package.json files, builds a
dependency graph, and reports any cycles:
✗ 1 workspace dependency cycle(s) found:
@legaciti/feature-foo → @legaciti/feature-bar → @legaciti/feature-foo
Fix by removing one of the cyclic dependencies. See docs/architecture/03-dependency-rules.mdRemove the dep that closes the cycle. If the shared code needs to be available
to both packages, extract it into a third package (e.g. a platform-* or
domain-* package) with no feature dependencies.
Running locally
Section titled “Running locally”# Full local validation (same as CI validate + architecture-checks jobs)pnpm validate
# Architecture checks only (lint + hygiene + cycles)pnpm ci:arch
# Just import-boundary lintpnpm lint:arch
# Just structural hygienepnpm check:arch
# Just cycle detectionpnpm check:cyclesFuture checks (staged / not yet enforced)
Section titled “Future checks (staged / not yet enforced)”The following checks are desirable but not yet ready to block CI:
| Check | Reason not yet enforced | Recommended next step |
|---|---|---|
| Workspace dependency direction graph | depcruise config needed | Evaluate after package extraction stabilizes |
| In-feature barrel indirection | Same-feature absolute @/features/own/ paths allowed | Enforce relative-only once all same-feature violations fixed |
These are tracked in 06-current-gaps.md.