codeflash-agent/packages/.claude/rules/philosophy.md
2026-04-03 17:36:50 -05:00

8.3 KiB

paths
*/src/**/*.py
*/tests/**/*.py

Engineering Philosophy

The deeper reasoning behind the codeflash repos' technical choices. Understanding the why enables applying the thinking to novel situations, not just pattern-matching on existing code.

Library Thinking vs Application Thinking

These are libraries. The engineering decisions flow from one question: what will the user of this code experience?

A library author thinks backwards from the import statement:

# This is where design starts
import svcs
registry = svcs.Registry()

Then works inward: what does Registry need? That goes in _core.py. What can go wrong? That goes in exceptions.py. What types does the user need? Those get re-exported from __init__.py. Everything else is hidden.

This applies even when you're not writing a published library. Every module boundary is a library boundary. Every function signature is an API. The discipline scales down.

Why Immutability

Mutable shared state is the root cause of an entire class of bugs: race conditions, action-at-a-distance, ordering dependencies, reentrancy hazards. Frozen objects eliminate all of these by construction.

The cost is allocations. bind() creates a new object instead of mutating. In Python, this cost is negligible compared to the bugs it prevents. The GC handles short-lived objects well.

The deeper insight: immutability makes code predictable. When you see a frozen object, you know its state won't change between the line you're reading and the line below. That's a powerful reasoning tool.

When to break the rule: objects with lifecycle (registries, connection pools, containers). These need state transitions. Use @attrs.define and make the mutation methods part of the documented interface.

Why attrs Over dataclasses

attrs predates dataclasses and remains strictly more capable:

  • Validators at construction time (not just post-init hacks)
  • Frozen + slots in one decorator (@attrs.frozen)
  • Private attribute aliases (attrs.field(alias="public_name") for _private_attr)
  • Factory defaults that actually work (attrs.Factory(list))
  • Composable validators (attrs.validators.and_, attrs.validators.optional)

dataclasses are fine for simple structs. When your data has invariants, validation, or privacy needs, attrs is the right tool.

Why 79 Characters

This isn't nostalgia. It's ergonomics.

  • Side-by-side diffs in a code review fit without horizontal scrolling
  • Three-pane merge tools work without wrapping
  • Terminal-based workflows (ssh, tmux splits) remain usable
  • Shorter lines force decomposition — long lines often mean too much is happening

The 88-character default from Black was a compromise for codebases that were already wide. Starting at 79 is starting with discipline.

Why ruff ALL + Ignores

Most projects pick rules one by one. This is backwards. You miss rules you didn't know existed.

Starting with ALL and ignoring what doesn't apply means:

  • New rules in future ruff versions are automatically enabled
  • You're forced to justify every exception (the ignore comment is documentation)
  • Coverage is comprehensive by default, not aspirational

The ignore list is a design document. Each entry says "we considered this rule and rejected it for this reason." That's more valuable than a hand-picked enable list.

Why 100% Docstring Coverage

interrogate --fail-under=100 forces every public symbol to have a docstring. This isn't busywork — it's a design pressure.

When you can't write a clear one-sentence docstring for a function, the function is probably doing too much or is poorly named. The docstring requirement surfaces design problems early.

It also means users can always help() any public symbol and get something useful. For a library, that's table stakes.

Why Strict mypy

strict = true enables every check mypy has. The pain is upfront (annotating everything, handling edge cases). The payoff is continuous:

  • Refactoring is safe. mypy catches breakage across module boundaries.
  • APIs are self-documenting. The signature tells you what goes in and comes out.
  • Any becomes a deliberate escape hatch, not an invisible default.

The version-conditional import pattern (if sys.version_info >= (3, 11)) exists because strict mypy demands it. try/except ImportError is too loose — it hides real import failures behind the version-gate.

Why Private Modules

Every module in src/package/_core.py starts with underscore. This communicates:

  1. To users: don't import from here. Use the package-level imports.
  2. To maintainers: this can be refactored freely. No external code depends on the module path.
  3. To tools: IDE autocompletion won't suggest package._core.Thing when package.Thing is available.

The __init__.py re-export is the load-bearing interface. The private modules are the implementation that can be split, merged, renamed, or reorganized without breaking anyone.

Exception: exceptions.py has no underscore because exception classes are part of the public API by nature — users need them in except clauses and imports.

Why Minimal Dependencies

Every dependency you add:

  • Can break with an update
  • Can be abandoned by its maintainer
  • Increases install time and size
  • Adds to your security surface area
  • Can conflict with other packages in the user's environment

For a library, this matters more than for an application. Your dependency becomes your user's transitive dependency. They didn't choose it and they can't easily remove it.

The test: does this dependency do something that would take 200+ lines to replicate? If yes, depend on it. If you'd just be wrapping a 20-line function, write the 20 lines.

Why uv_build

uv_build is the build backend for all packages in this workspace:

  • Standards-compliant (PEP 621 metadata)
  • Fast and minimal — no setuptools baggage
  • Consistent with uv as the single tool for all Python operations (run, sync, lock, build)

Version is declared in pyproject.toml directly. No dynamic version derivation.

Design Pressure as a Tool

Many of these choices create design pressure — they make bad design uncomfortable:

  • 79 chars pressures you to write shorter expressions, extract functions, use better names
  • 100% docstrings pressures you to make every public symbol worth documenting
  • Strict mypy pressures you to design clean interfaces with clear types
  • Frozen classes pressure you to think about state ownership upfront
  • Private modules pressure you to think about what's truly public

The constraints aren't obstacles. They're tools that push design toward clarity.

On Simplicity

The goal isn't cleverness. The goal is code that a maintainer (including future-you) can read at 2am during an incident and understand immediately.

  • No metaclass magic unless the alternative is worse
  • No decorator stacking beyond two deep
  • No dynamic attribute generation
  • Prefer boring, explicit code over elegant, implicit code

The best code is code that looks obvious in retrospect.

Testing Philosophy

Tests serve three audiences:

  1. The CI system: does it work?
  2. Future maintainers: what is this supposed to do?
  3. Users reading tests as examples: how do I use this?

The docstring on each test method serves audiences 2 and 3. The assertion serves audience 1. Writing """bind() returns a new BoundLogger with merged context.""" is more valuable than test_bind_returns_new_instance alone.

Class-based test organization mirrors the code structure. TestRegistry contains all tests for Registry. This makes it trivial to find the tests for a given class, and to understand the full behavioral surface.

On Framework Integrations

Framework integrations (FastAPI, Flask, Starlette) live in separate modules, not in core:

svcs/
    _core.py      # Framework-agnostic core
    fastapi.py    # FastAPI integration
    flask.py      # Flask integration
    starlette.py  # Starlette integration

This keeps the core free of framework dependencies. A Flask user never imports FastAPI code. The integration module adapts the core to the framework's conventions (dependency injection, request lifecycle, etc.) without polluting the core API.

This pattern applies broadly: keep the engine separate from the interface. The core should work without any framework. The integration is a thin adapter.