# Baseline Dev Architecture

> 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>".

- Skill: `oimiragieo/baseline-dev-architecture` (Agent Skill)
- Install (CLI): `npx skillmds@latest add oimiragieo/baseline-dev-architecture`
- Raw SKILL.md: https://api.skillmd.com/api/skills/oimiragieo/baseline-dev-architecture/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Integrations & APIs
- Author: oimiragieo (https://skillmd.com/u/oimiragieo)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/oimiragieo/baseline-dev-architecture

---


# 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

1. **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.
2. **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.**
3. **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).
4. **Thin entrypoint, testable core.** `main`/CLI only parses args + wires things together; all real logic lives in importable, testable units.
5. **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.
6. **Co-locate what changes together** — including fast unit tests.
7. **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:
1. **It builds/compiles.**
2. **It boots** — `--help`/`--version` (or a bare import) returns exit 0.
3. **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`.

