| name | baseline-dev-architecture |
| description | Use when starting or restructuring any software project in ANY language — deciding folder/module layout, dependency direction, public API boundaries, where logic vs entrypoint lives, the testing tiers, smoke tests, or CI gates — when no language-specific architecture skill applies. Language-agnostic parent of typescript-cli-architecture and rust-workspace-architecture. Triggers — "how should I structure this", "where does this file/module go", "feature vs layer", "set up tests and CI", "smoke test", "new package or module?", "structure my Go/Python project", "project layout for <language>". |
Baseline Dev Architecture (language-agnostic)
Overview
The architecture decisions that don't change with language, abstracted from typescript-cli-architecture and rust-workspace-architecture. This is the mental model; the per-language skills are concrete instances.
Core principle: organize by what changes together, point dependencies one direction, hide internals behind small public surfaces, keep the entrypoint thin and the core testable.
When to Use
- Starting or restructuring a project in any language.
- Deciding layout, boundaries, testing, or CI when no language-specific skill exists.
Also load the specific skill when it exists: TypeScript/Node/Bun → typescript-cli-architecture; Rust → rust-workspace-architecture. Skip for a throwaway / single-purpose script with no second consumer — a flat layout is fine.
The 7 Invariants
- Organize by feature/capability, not technical layer. Adding a feature means adding a folder; deleting one means deleting a folder. Avoid
controllers/ services/ models/ trees that scatter one feature across the repo.
- Dependency direction is the load-bearing decision. Pure types/domain at the bottom → adapters (db/io/auth) in the middle → entrypoints (cli/server) at the top. Lower layers never import higher. No cycles.
- Explicit public surface per unit. Each module/package exposes a small public API; internals are hidden; siblings never reach into each other's internals. Cross-cutting code goes through a deliberately small shared layer (the moment it becomes a framework, you've recreated layer-first).
- Thin entrypoint, testable core.
main/CLI only parses args + wires things together; all real logic lives in importable, testable units.
- Config & errors live at the boundary. Config/secrets come from env, flags, or files injected at the entrypoint — never hardcoded or read deep in the core. Validate inputs at the boundary, fail fast on bad config, and return typed/structured errors from public APIs (internal helpers may use loose handling). Wire logging/observability at the entrypoint and pass it down — no scattered
print/println in the core.
- Co-locate what changes together — including fast unit tests.
- Split a unit (module → package/crate/service) only when the split earns it: different change rate, multiple consumers, independent test/deploy, or compile/parallelism win. Reach for a module first; premature packaging is worse than a little duplication.
Testing (tiers exist in every language)
| Tier | What | Note |
|---|
| Unit | fast, pure logic, many; co-located | inject dependencies so seams are explicit |
| Integration | real deps (db/queue/fs/network) | best bug-catch ROI — don't over-mock here |
| E2E | few, critical user/CLI paths only | slow & fragile; reserve for what matters |
| + extras | snapshot / property / doc tests | use the ones your language offers |
Mock only at boundaries with no interesting runtime behavior; use real infrastructure where implementation details bite. Isolate tests from global state (temp dirs, injected config) so they're not flaky.
Smoke Test (universal — run FIRST)
The cheapest real gate, before the full suite or any expensive job:
- It builds/compiles.
- It boots —
--help/--version (or a bare import) returns exit 0.
- One end-to-end happy path on the simplest real command/operation.
CI Order (universal)
format → lint/typecheck → smoke → unit → integration → e2e. Consider coverage gating (a policy choice, not an invariant), pin the toolchain, and centralize dependency versions + commit the lockfile for applications.
Per-Language Mapping (quick reference)
| Concern | TS/Node/Bun | Rust | Python | Go |
|---|
| feature unit | feature folder + index.ts | crate / module | package (__init__.py) | package dir |
| public surface | index.ts barrel | pub vs pub(crate) | __init__.py re-exports + _private naming (__all__ only documents, doesn't enforce) | exported (Capitalized) ids; internal/ dir hides packages |
| cycle enforcement | eslint-plugin-import/no-cycle / madge | compiler-enforced | NOT enforced — use import-linter | compiler-enforced (free) |
| thin entrypoint | main.ts + app.ts | main.rs + lib.rs | __main__.py + lib | cmd/ main pkg + lib pkgs |
| dep versions | package.json + lockfile | [workspace.dependencies] + Cargo.lock | pyproject.toml + lock (uv/poetry) | go.mod + go.sum |
| typed errors | Error subclasses | thiserror enum + Result alias | exception hierarchy | sentinel/wrapped errors (errors.Is/As) |
| unit test | *.test.ts (Vitest) | #[cfg(test)] | test_*.py (pytest) | *_test.go |
| integration | *.integration.test.ts | crate-root tests/ | tests/ | *_test.go (+build tag) |
| runner | vitest |
Common Mistakes
| Mistake | Fix |
|---|
| Layer-first folders that scatter a feature | Group by feature/capability |
| Domain depends on framework / cyclic deps | Enforce one-way downward dependency |
Fat entrypoint with logic in main | Thin wire-up; logic in importable lib |
| Packaging into separate modules too early | Module first; split only when it earns it |
| No smoke test → full suite runs on a broken build | Build + boots + 1 happy path, run first |
| Tests mutate global state / real home dir | Isolate: temp dirs, injected deps |
AI Anti-Patterns to Flag When Auditing
AI agents (and AI-assisted humans) leave a recognizable residue. When auditing a codebase, actively scan for these — they are habits, not one-off mistakes, so finding one usually means there are more. (Some observed directly in a real v1.13 audit, cited "seen"; others confirmed by 2026 studies, cited "research".)
Root cause: AI generates code that is locally correct for the prompt, not globally coherent with the system it can't fully see — and quality decays as volume grows. Receipts: GitClear (211M lines) — AI code clones up 4–8× while refactoring collapsed 25%→<10%; CodeRabbit (470 PRs) — 1.7× more issues per AI PR; OX Security — over-specification in 80–90% of AI repos; arXiv 2605.02741 — a near-perfect correlation between code volume and architectural decay ("Volume–Quality Inverse Law").
| Anti-pattern | The AI tell (where it shows up) | Flag / fix |
|---|
| Root scratch sprawl (seen) | out.txt err.txt *_out.txt tg_*.txt *.log tmp_* dummy.* sample*.txt at the repo root, sometimes 10s–100s of MB | AI pipes command output to CWD to "read" results and abandons it. Root-anchored .gitignore (/scratch, /*.log); delete; never let scratch reach root |
| Repro/debug dir graveyard (seen) | *_repro/, *_repro2..N/, .tmp_*/, debug_*/, scratch_*/ directories | Created while iterating, never cleaned. Delete + ignore |
| God-files that only grow (seen) | one module far past the size ceiling (5–10k+ LoC main/utils/handlers/repo_map) | AI appends to the file it already has open instead of splitting. Flag any module past the ceiling; split by responsibility |
| Duplicate "version" files | foo_v2, foo_new, foo_fixed, foo_final, foo.bak, foo copy.ts beside foo | AI copies-then-edits instead of replacing. Keep one; delete the rest (git is the history) |
| Convenience-typed boundaries (seen) | anyhow/any/interface{}/except Exception/# type: ignore in public signatures | Easiest type applied uniformly. Use typed errors/inputs at the public edge (invariant 5) |
| Tests that never run the real path | weak/tautological assertions (assert x is not None, toBeDefined, assertTrue(x)), tests that grep source, assert on constants, or mock the very thing under test |
Audit stance: distinguish severity honestly — untracked scratch in the working tree is low-severity (just ignore/delete); a 10k-LoC god-file or layer-first sprawl in a mature shipping codebase is a real but high-churn finding → recommend incremental fixes, not a risky rewrite. Verify tracked-vs-untracked before recommending any git rm.
Defend, don't just flag. These habits recur every session, so prevention must be machine-checked, not hoped for:
- Encode boundaries as architectural fitness functions in CI — import-cycle/layer linters (
import-linter for Python, dependency-cruiser/eslint-plugin-boundaries for TS, ArchUnit for JVM, compiler for Rust/Go), banned-raw-API rules (Ruff TID251), dead-code/unused (F401/F841/ERA001), assertion-presence (jest/expect-expect).
- Review structurally, not just behaviorally: the question isn't "does it run?" (AI code usually does) but "does it fit our existing patterns — do we already have this?"
- Track clone-rate and refactor-rate as health metrics, not just lines shipped.
Concrete implementations
typescript-cli-architecture, rust-workspace-architecture. When working in another language, apply the invariants above and the mapping row; if that language becomes a recurring target, spin up its own skill via superpowers:writing-skills.