Setup Arch-Unit Architecture Tests
Instructions
Install and configure arch-unit-ts so that the project's module boundaries are enforced as part of the test suite, and wire a dedicated pre-commit hook that runs the architecture rules on every commit.
This skill is independent of setup-pre-commit: it manages its own block
inside .husky/pre-commit (delimited by # >>> nexa arch-unit >>> markers) and
does not touch any lint-staged configuration. Running both skills on the same
project is safe.
Nexa Rules Gate
Read and follow ${CLAUDE_PLUGIN_ROOT}/shared/readiness/NEXA_RULES_GATE.md.
Prerequisites
The following must exist before running this skill:
package.json— to determine the package manager and detect installed deps- A Vitest setup — arch-unit-ts tests run as Vitest specs. If Vitest is not
installed, stop and tell the user to run
/vitest-testfirst or install Vitest manually.
If package.json is missing or Vitest is not installed, stop and tell the user
which prerequisite is missing.
DO NOT
- Do not skip the verification step (Step 8) — arch-unit-ts package matchers may silently match zero files for folder names containing parens (route groups) or leading underscores (private folders). The sanity checks in the generated template catch this, but only if the spec is actually executed once.
- Do not modify or share configuration with the
setup-pre-commitskill — this skill manages its own labeled block inside.husky/pre-commit. - Do not add the architecture spec to
lint-staged— arch-unit-ts analyzes the whole project, not the staged subset. - Do not enable opt-in rule blocks (route groups, private folders, layered architecture) without the user's explicit confirmation.
- Do not add a CI workflow — the user has explicitly scoped this skill to pre-commit only.
- Do not invent package matcher patterns. Only use patterns from the arch-unit-ts documentation, fetched via context7 in Step 0.
- Do not generate a spec that imports paths that don't exist in the project
(e.g.
..lib..if there is nolib/folder). Detect first, then write.
Step 0: Consult arch-unit-ts Documentation
Before writing any code, use the context7 MCP server to look up:
- arch-unit-ts (library id
/arch-unit-ts/arch-unit-ts) — confirm the current API for:classes(),noClasses(),Architectures.layeredArchitecture()- Package matchers:
resideInAPackage,resideInAnyPackage,resideOutsideOfPackage(if available) - Dependency matchers:
dependOnClassesThat,onlyDependOnClassesThat,onlyHaveDependentClassesThat TypeScriptProjectconstructor signature and exclusion patternsfilterClasses(glob)API for the fallback patternTypeScriptClass.hasImport(name)andpackagePath.getDotsPath()
- arch-unit-ts version — read
package.jsonafter installation; the import paths in the template (arch-unit-ts/dist/...) may differ across versions
If the API has changed since this skill was written, prefer what the documentation says for the installed version over what the template shows.
Workflow
Step 1: Gather Context
Detect the project layout. Read or run:
package.json— record:- Whether
arch-unit-tsis already a dep - Whether
huskyis already a dep - Whether Vitest is installed (look for
vitestor@vitest/*) - The package manager (presence of
package-lock.json,pnpm-lock.yaml,yarn.lock,bun.lockb)
- Whether
- Filesystem detection:
- Does
src/exist? If yes, theTypeScriptProjectroot issrc. If no, the root is.. - Which of the common shared folders exist at the source root:
components/,lib/,hooks/,ui/,utils/,styles/? - Does
app/exist? List immediate children matching(*)/— these are route groups. - List immediate children of
app/**/matching_*/— these are private folders. - Does any of these exist:
domain/,application/,infrastructure/? If yes, the layered-architecture opt-in is a strong candidate.
- Does
- Existing test file check:
- Does
tests/architecture/architecture.test.ts(or.spec.ts) already exist? If yes, read it and ask the user whether to overwrite, extend, or skip — do not overwrite silently.
- Does
- Existing Husky setup:
- Does
.husky/exist? Does.husky/pre-commitexist? If yes, read it and check for an existing# >>> nexa arch-unit >>>block.
- Does
Record all findings before proceeding.
Step 2: Propose Default Rule Set
Present a single proposal to the user showing what the generated test will contain. Structure the message like this:
Proposed architecture rules (based on the Next.js project structure conventions and what I detected in your project):
Enabled by default
- R1 — Shared code must not depend on
app/([SHARED_DIRS_LIST]) Detected shared folders: [list].app/consumes them; reversing the dependency would couple them to your routes.Opt-in (commented out in the generated file)
- R2 — Route groups must not cross-import [only show if route groups detected: list them, e.g.
(marketing),(shop)] Caveat: arch-unit-ts dot-path matchers may not handle parens cleanly. The generated test includes a sanity check that catches silent zero-match passes; a manual fallback usingfilterClasses()is provided in the file.- R3 — Private folders are implementation details [only show if
_*folders detected: list them] Same parens-style caveat applies for leading underscores.- R4 — Layered / hexagonal architecture [only show if
domain/,application/, orinfrastructure/detected] Domain ⟂ application ⟂ infrastructure boundaries.Custom rules Want to add or modify anything before I generate the file? You can:
- Enable any opt-in rule (R2/R3/R4)
- Add a custom rule (tell me which packages must not depend on which)
- Remove a default rule
- Just confirm and generate
Wait for the user to respond before proceeding. Apply their changes to the template plan.
Step 3: Confirm Strategy
Summarize the final decisions in a compact table and ask once more before writing files:
Final architecture test setup
Setting Value Test file path tests/architecture/architecture.test.tsTypeScriptProjectroot`[. Rules enabled [list of enabled rule IDs] npm script test:arch→vitest run tests/architecturePre-commit hook dedicated block in .husky/pre-commit(separate from anyexisting lint-staged setup) Proceed?
Wait for confirmation.
Step 4: Install Dependencies
Install arch-unit-ts and (if missing) husky as dev dependencies. Use the
detected package manager:
npm install -D arch-unit-ts huskypnpm add -D arch-unit-ts huskyyarn add -D arch-unit-ts huskybun add -d arch-unit-ts husky
Ask the user for confirmation before installing. Skip packages that are already installed.
After install, re-query context7 for the installed version of arch-unit-ts to confirm the import paths in the template are still valid.
Step 5: Scaffold the Architecture Spec
Read the template at ${CLAUDE_PLUGIN_ROOT}/skills/setup-arch-unit/templates/architecture.test.template.ts.
Write it to tests/architecture/architecture.test.ts, filling in the following
placeholders based on Step 1 detection:
| Placeholder | Replacement | Example |
|---|---|---|
{{SRC_ROOT}} |
'src' if src/ exists, otherwise '.' |
'src' |
{{SHARED_PACKAGES}} |
Comma-separated '..foo..' strings for each detected shared folder |
'..components..', '..lib..', '..hooks..' |
{{SHARED_SANITY_PREDICATE}} |
JS expression on path that returns true for any shared package |
path.includes('.components.') || path.includes('.lib.') || path.includes('.hooks.') |
For each opt-in rule the user enabled in Step 2, uncomment that block in the
generated file and parameterize it (e.g. replace (GROUP_A)/(GROUP_B) with
the actual route group names).
If the user added custom rules, append them as additional it(...) blocks
inside the describe('Architecture', ...) and clearly label them with
// Custom rule N — <summary> comments.
Step 6: Add the npm Script
Read package.json and add a test:arch script to the scripts object:
{
"scripts": {
"test:arch": "vitest run tests/architecture"
}
}
If a test:arch script already exists with a different command, ask the user
whether to overwrite or rename.
Do not modify any other scripts.
Step 7: Set Up the Pre-commit Hook
This step has two cases.
7a — No .husky/ directory
Initialize Husky:
- npm:
npx husky init - pnpm:
pnpm dlx husky init - yarn:
yarn dlx husky init - bun:
bunx husky init
This creates .husky/pre-commit with a default npm test line. Replace its
contents with only the arch-unit block from
${CLAUDE_PLUGIN_ROOT}/skills/setup-arch-unit/templates/precommit-block.sh.
7b — .husky/pre-commit exists
Read the file. Three sub-cases:
- No nexa arch-unit block — append the contents of
templates/precommit-block.shto the end of the file. Preserve everything already in the hook. - An existing nexa arch-unit block (detect via
# >>> nexa arch-unit >>>marker) — replace only the lines between the markers (inclusive) with the fresh block. This makes re-running the skill idempotent. - No Husky setup but
.husky/pre-commitexists from another tool — ask the user whether to proceed or abort.
Make the hook executable: chmod +x .husky/pre-commit.
Step 8: Run It Once to Verify (mandatory)
Execute npm run test:arch (or the equivalent for the detected package
manager). Three outcomes:
- All sanity checks pass and all rules pass — proceed to Step 9.
- A sanity check fails with "should contain files, got 0" — the dot-path
matcher did not match a folder whose name contains parens or leading
underscores. Tell the user and recommend the manual fallback at the bottom
of the generated spec file (using
filterClasses()+hasImport()). Offer to rewrite the failing rule using the fallback pattern. - A rule fails with an actual architectural violation — show the user the
violation. Ask whether to:
- Fix the violating code (out of scope for this skill — recommend
/implementor a manual edit) - Mark the rule as a known follow-up (comment it out with a
// TODO:and a linked technical task) - Reconsider whether the rule is correct
- Fix the violating code (out of scope for this skill — recommend
Do not declare the skill complete until the test command exits cleanly (either all pass, or the rules causing failures are explicitly deferred with TODOs).
Step 9: Update CLAUDE.md
Append the following section to the target project's CLAUDE.md (create the
file if it doesn't exist).
If a ## Architecture Tests section already exists (check for
<!-- NEXA_ARCH_UNIT_CONFIGURED -->), ask the user whether to overwrite or
skip — do not silently duplicate.
## Architecture Tests
<!-- NEXA_ARCH_UNIT_CONFIGURED -->
- Library: `arch-unit-ts`
- Spec file: `tests/architecture/architecture.test.ts`
- Run: `npm run test:arch` (also runs as a Vitest spec under the regular suite)
- Pre-commit: enforced via `.husky/pre-commit` (dedicated block, independent
of any lint-staged setup)
- Active rules: [list of enabled rule IDs and one-line descriptions]
### Conventions
- New shared modules (under `components/`, `lib/`, `hooks/`, `utils/`, `ui/`)
must never import from `app/` — only the other direction.
- When adding a new route group (`app/(group)/`), update the route-group
cross-import rule if R2 is enabled.
- When adding a layered module (`domain/`, `application/`, `infrastructure/`),
enable or extend R4 to reflect the new layer's allowed dependencies.
Do not remove or modify any other content in CLAUDE.md.
Step 10: Summary
Present a summary of what was created:
## Architecture Tests Configured
### Strategy
- Library: arch-unit-ts vX.Y.Z (installed)
- Test framework: Vitest
- Active rules: [list]
- Pre-commit: dedicated block in .husky/pre-commit
### Files Created/Updated
| File | Change |
|---|---|
| tests/architecture/architecture.test.ts | created (N rules enabled) |
| package.json | added `test:arch` script |
| .husky/pre-commit | [created / appended nexa arch-unit block] |
| CLAUDE.md | appended `## Architecture Tests` section |
### Verification
- `npm run test:arch`: [X passed, Y failed]
- Sanity checks: [all matched files / N folders matched 0 files — see fallback]
### Next Steps
- To enable a commented opt-in rule (route groups, private folders, layered
architecture), uncomment it in tests/architecture/architecture.test.ts and
re-run `npm run test:arch`.
- The hook runs on every commit. To bypass once (not recommended), use
`git commit --no-verify`.
- If a sanity check ever flags a zero-match folder after a project rename,
update the matcher patterns in the spec — folder structure changes can
silently break package patterns.
Reference Templates
- Architecture spec: templates/architecture.test.template.ts
- Pre-commit block: templates/precommit-block.sh
Spec Template Placeholders
| Placeholder | Description | Example |
|---|---|---|
{{SRC_ROOT}} |
Root path passed to RelativePath.of() |
'src' or '.' |
{{SHARED_PACKAGES}} |
Comma-separated dot-path patterns for shared folders | '..components..', '..lib..' |
{{SHARED_SANITY_PREDICATE}} |
JS expression on a path string that returns true for any shared package |
path.includes('.components.') || path.includes('.lib.') |
Design Decisions
Why a separate hook block instead of integrating with setup-pre-commit
setup-pre-commit configures lint-staged for fast, staged-file-only checks
(formatter, type-check on staged files, etc.). Arch-unit-ts is fundamentally a
whole-project analysis — it must scan all files to resolve dependencies. Putting
it inside lint-staged would either (a) feed it only staged files, breaking the
analysis, or (b) run the full project scan on every staged-file invocation,
which mixes concerns.
A dedicated labeled block in .husky/pre-commit keeps the two concerns
independent: each skill manages only its own block, marked by
# >>> nexa arch-unit >>> / # <<< nexa arch-unit <<<. Re-running this skill
replaces only its block; re-running setup-pre-commit does not touch it.
Why the spec ships with a sanity check on every rule
arch-unit-ts package matchers like '..foo..' are silent when they match
nothing — the rule passes vacuously and the user gains false confidence. Next.js
folder names commonly contain parens (route groups) and leading underscores
(private folders), both of which may interact unpredictably with the dot-path
matcher. The sanity check (expect(matchedClasses.length).toBeGreaterThan(0))
turns a silent miss into a loud, named test failure on the very first run.
Why no CI workflow
Explicitly scoped out by the user. The pre-commit hook is the enforcement
point. If the user later wants CI enforcement, npm run test:arch is already a
standalone command and can be added to any CI job manually.