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 |
cargo nextest |
pytest |
go test |
| smoke |
build + --version |
cargo check + --version |
import + --version |
go build + --version |
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 |
Green tests, broken runtime ("coverage lie"). Assert on actual values; require ≥1 test exercising the real path; add a smoke gate |
| No smoke gate (seen) |
CI jumps straight to the full matrix; nothing checks "does it build + boot" first |
Add the smoke test (build → boots → 1 happy path) as the first CI job |
.gitignore wrong in both directions (seen) |
generated artifacts committed (coverage, logs, build/) AND the app lockfile ignored |
AI adds files but doesn't maintain ignore rules. Ignore generated output; commit lockfiles for applications |
| Dead code left in place |
large commented-out blocks, unused functions, # TODO: remove, _old_ shadows |
AI is reluctant to delete. Remove it — git remembers |
| Layer-first by reflex (seen) |
controllers/ services/ models/ for an app that is clearly feature-shaped |
The most-represented training pattern, applied without fit-check. Re-evaluate vs feature-first (invariant 1) |
| Inline config/secrets/paths |
hardcoded paths, URLs, model names, keys read deep inside modules |
Convenience over boundary. Inject at the entrypoint (invariant 5) |
| Sibling experiment repos |
proj, proj-rs, proj-v2, proj-ci-repro, proj-replay as separate top-level clones |
Spun up for one experiment, never folded back or deleted. Consolidate or archive deliberately |
| Parallel implementations of one concept (research) |
the same logic (auth, validation, HTTP client, date format, retry) implemented 3+ ways in different files |
Context-window blindness: AI doesn't see the existing impl, so it writes a new one. The #1 AI smell. Grep for the concept before adding; consolidate to one; enforce with a banned-API lint |
| Abstraction bypass (research) |
reaches for the raw library / inline call instead of the project's existing wrapper (DB query inside an HTTP handler; raw fetch instead of BaseClient) |
The shared layer exists but wasn't in context. Route through the established layer; ban the raw API in lint |
| Porous public surface |
package exports via import * / no __all__; everything pub or exported; internals freely imported across modules |
No declared boundary → siblings reach into internals and coupling spreads. Declare the surface (__all__ + _private, pub(crate), internal/) and enforce with a boundary linter (invariant 3) |
| Modular mirage (research) |
files ARE split, but related behavior is scattered across them with no cohesion — structural modularity ≠ logical modularity |
"Can I point to the one place capability X lives?" If no, re-cohere (invariant 1) |
| Over-engineering / speculative generality (research) |
factories/abstractions/config systems/defense-in-depth + handling impossible cases for a thing with ONE implementation; over-specification (80–90% of AI repos) |
Delete the unused generality. YAGNI. Build for what the spec requires, not hypotheticals |
| Phantom validation (research) |
static types treated as runtime validation; no schema check on external input at the boundary |
Types evaporate at runtime. Validate inputs at the edge with a runtime schema (invariant 5) |
| Hallucinated / deprecated APIs (research) |
calls to methods/flags that don't exist, or were removed in your installed version |
Trained on old/averaged code. The build/typecheck + smoke gate catches these; verify against current docs |
| Error suppression ("afraid to fail") (research) |
bare except/catch-and-continue, every failure → generic 500, errors logged-and-swallowed |
AI optimizes for "runs" over "correct". Distinguish failure modes; propagate; don't swallow |
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.
1---2name: baseline-dev-architecture3description: 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>".4---56# Baseline Dev Architecture (language-agnostic)78## Overview910The 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.1112Core principle: *organize by what changes together, point dependencies one direction, hide internals behind small public surfaces, keep the entrypoint thin and the core testable.*1314## When to Use1516- Starting or restructuring a project in any language.17- Deciding layout, boundaries, testing, or CI when no language-specific skill exists.1819**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.2021## The 7 Invariants22231. **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.242. **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.**253. **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).264. **Thin entrypoint, testable core.** `main`/CLI only parses args + wires things together; all real logic lives in importable, testable units.275. **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.286. **Co-locate what changes together** — including fast unit tests.297. **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.3031## Testing (tiers exist in every language)3233| Tier | What | Note |34|---|---|---|35| Unit | fast, pure logic, many; co-located | inject dependencies so seams are explicit |36| Integration | real deps (db/queue/fs/network) | **best bug-catch ROI** — don't over-mock here |37| E2E | few, critical user/CLI paths only | slow & fragile; reserve for what matters |38| + extras | snapshot / property / doc tests | use the ones your language offers |3940Mock 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.4142## Smoke Test (universal — run FIRST)4344The cheapest real gate, before the full suite or any expensive job:451. **It builds/compiles.**462. **It boots** — `--help`/`--version` (or a bare import) returns exit 0.473. **One end-to-end happy path** on the simplest real command/operation.4849## CI Order (universal)5051`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.5253## Per-Language Mapping (quick reference)5455| Concern | TS/Node/Bun | Rust | Python | Go |56|---|---|---|---|---|57| feature unit | feature folder + `index.ts` | crate / module | package (`__init__.py`) | package dir |58| 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 |59| cycle enforcement | `eslint-plugin-import/no-cycle` / madge | compiler-enforced | NOT enforced — use `import-linter` | compiler-enforced (free) |60| thin entrypoint | `main.ts` + `app.ts` | `main.rs` + `lib.rs` | `__main__.py` + lib | `cmd/` main pkg + lib pkgs |61| dep versions | `package.json` + lockfile | `[workspace.dependencies]` + `Cargo.lock` | `pyproject.toml` + lock (uv/poetry) | `go.mod` + `go.sum` |62| typed errors | `Error` subclasses | `thiserror` enum + `Result` alias | exception hierarchy | sentinel/wrapped errors (`errors.Is/As`) |63| unit test | `*.test.ts` (Vitest) | `#[cfg(test)]` | `test_*.py` (pytest) | `*_test.go` |64| integration | `*.integration.test.ts` | crate-root `tests/` | `tests/` | `*_test.go` (+build tag) |65| runner | vitest | cargo nextest | pytest | go test |66| smoke | build + `--version` | `cargo check` + `--version` | import + `--version` | `go build` + `--version` |6768## Common Mistakes6970| Mistake | Fix |71|---|---|72| Layer-first folders that scatter a feature | Group by feature/capability |73| Domain depends on framework / cyclic deps | Enforce one-way downward dependency |74| Fat entrypoint with logic in `main` | Thin wire-up; logic in importable lib |75| Packaging into separate modules too early | Module first; split only when it earns it |76| No smoke test → full suite runs on a broken build | Build + boots + 1 happy path, run first |77| Tests mutate global state / real home dir | Isolate: temp dirs, injected deps |7879## AI Anti-Patterns to Flag When Auditing8081AI 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".)8283**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").8485| Anti-pattern | The AI tell (where it shows up) | Flag / fix |86|---|---|---|87| **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 |88| **Repro/debug dir graveyard** *(seen)* | `*_repro/`, `*_repro2..N/`, `.tmp_*/`, `debug_*/`, `scratch_*/` directories | Created while iterating, never cleaned. Delete + ignore |89| **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 |90| **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) |91| **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) |92| **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 | Green tests, broken runtime ("coverage lie"). Assert on actual values; require ≥1 test exercising the real path; add a smoke gate |93| **No smoke gate** *(seen)* | CI jumps straight to the full matrix; nothing checks "does it build + boot" first | Add the smoke test (build → boots → 1 happy path) as the first CI job |94| **`.gitignore` wrong in both directions** *(seen)* | generated artifacts committed (coverage, logs, `build/`) AND the app **lockfile ignored** | AI adds files but doesn't maintain ignore rules. Ignore generated output; **commit lockfiles for applications** |95| **Dead code left in place** | large commented-out blocks, unused functions, `# TODO: remove`, `_old_` shadows | AI is reluctant to delete. Remove it — git remembers |96| **Layer-first by reflex** *(seen)* | `controllers/ services/ models/` for an app that is clearly feature-shaped | The most-represented training pattern, applied without fit-check. Re-evaluate vs feature-first (invariant 1) |97| **Inline config/secrets/paths** | hardcoded paths, URLs, model names, keys read deep inside modules | Convenience over boundary. Inject at the entrypoint (invariant 5) |98| **Sibling experiment repos** | `proj`, `proj-rs`, `proj-v2`, `proj-ci-repro`, `proj-replay` as separate top-level clones | Spun up for one experiment, never folded back or deleted. Consolidate or archive deliberately |99| **Parallel implementations of one concept** *(research)* | the same logic (auth, validation, HTTP client, date format, retry) implemented 3+ ways in different files | Context-window blindness: AI doesn't see the existing impl, so it writes a new one. The #1 AI smell. Grep for the concept before adding; consolidate to one; enforce with a banned-API lint |100| **Abstraction bypass** *(research)* | reaches for the raw library / inline call instead of the project's existing wrapper (DB query inside an HTTP handler; raw `fetch` instead of `BaseClient`) | The shared layer exists but wasn't in context. Route through the established layer; ban the raw API in lint |101| **Porous public surface** | package exports via `import *` / no `__all__`; everything `pub` or exported; internals freely imported across modules | No declared boundary → siblings reach into internals and coupling spreads. Declare the surface (`__all__` + `_private`, `pub(crate)`, `internal/`) and enforce with a boundary linter (invariant 3) |102| **Modular mirage** *(research)* | files ARE split, but related behavior is scattered across them with no cohesion — structural modularity ≠ logical modularity | "Can I point to the one place capability X lives?" If no, re-cohere (invariant 1) |103| **Over-engineering / speculative generality** *(research)* | factories/abstractions/config systems/defense-in-depth + handling impossible cases for a thing with ONE implementation; over-specification (80–90% of AI repos) | Delete the unused generality. YAGNI. Build for what the spec requires, not hypotheticals |104| **Phantom validation** *(research)* | static types treated as runtime validation; no schema check on external input at the boundary | Types evaporate at runtime. Validate inputs at the edge with a runtime schema (invariant 5) |105| **Hallucinated / deprecated APIs** *(research)* | calls to methods/flags that don't exist, or were removed in your installed version | Trained on old/averaged code. The build/typecheck + smoke gate catches these; verify against current docs |106| **Error suppression ("afraid to fail")** *(research)* | bare `except`/catch-and-continue, every failure → generic 500, errors logged-and-swallowed | AI optimizes for "runs" over "correct". Distinguish failure modes; propagate; don't swallow |107108**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`.**109110**Defend, don't just flag.** These habits recur every session, so prevention must be machine-checked, not hoped for:111- **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`).112- **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?*"113- **Track clone-rate and refactor-rate** as health metrics, not just lines shipped.114115## Concrete implementations116117`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`.