feat(style): Add anti-OOP conventions and OOP refactoring tracker

- Add section 10 (Anti-OOP Conventions) to python.md with hard rules,
  class justification requirements, and Strangler Fig refactoring pattern
- Create conductor/refactor_oop.md tracker with 4 phases for class elimination
- Add ruff PLR rules (PLR0912, PLR6301, PLR0206) to pyproject.toml for
  OOP anti-patterns

Addresses AI agent scope misinterpretation issues by enforcing flat
function-call graphs over deep class hierarchies.
This commit is contained in:
2026-05-11 23:41:41 -04:00
parent 4ef18ab5d2
commit b9c1b63f8d
3 changed files with 121 additions and 5 deletions
+61 -4
View File
@@ -68,11 +68,68 @@ is processed by AI agents, while preserving readability for human review.
- **Minimal blank lines.** Token-efficient density is preferred over visual padding.
- **Short variable names are acceptable** in tight scopes (loop vars, lambdas). Use descriptive names for module-level and class attributes.
## 9. Line Length
## 10. Anti-OOP Conventions
- Soft limit: 120 characters.
- Hard limit: None — let the formatter handle wrapping if needed.
- Rationale: 80-char limits cause excessive line continuations that waste tokens.
### Philosophy
AI agents consistently misinterpret class hierarchies, method resolution, and inheritance. Flat function-call graphs are deterministic and traceable. OOP introduces scoping complexity that compounds with indentation.
### Hard Rules (Enforced by lint)
- **Never write a class for a single method.** Use a function.
- **Never use inheritance for code reuse.** Compose with standalone functions.
- **Never use private methods (`_method`).** Module-level functions with clear names suffice.
- **No nested classes.** Define helper types at module level.
- **No decorator classes.** Use plain functions with decorators.
### Class Justification Required
Every class definition MUST include a comment explaining WHY it is a class and not a function group or struct:
```python
# JUSTIFIED: Holds mutable shared state across multiple async operations
# Cannot be replaced by functions without passing state through every call
class AsyncOperationScheduler:
...
# NOT JUSTIFIED: Simply groups related functions
# Should be module-level functions in operations.py
class OperationHelper:
def validate(self): ...
def execute(self): ...
```
### Acceptability Criteria
A class is justified ONLY when ALL of:
1. It holds mutable state that must be encapsulated
2. It has 3+ related methods that share state
3. It implements a behavioral interface used polymorphically (not just data grouping)
### Refactoring Existing Classes (Strangler Fig Pattern)
When refactoring a class to functions:
1. Write test validating current behavior (prevents regression)
2. Extract one method at a time into module-level functions
3. Create wrapper function that delegates to class until migration complete
4. Delete class only when ALL callers migrated
5. Commit with `refactor(oop):` prefix
### Data Structures
- **Data-only containers:** Use `NamedTuple`, `dataclass(frozen=True)`, or plain `dict` — NOT classes
- **State machines:** Use dict-based transitions, not class + inheritance
- **Configuration:** Plain dict or `TypedDict`, not classes with defaults
### Anti-Patterns (Flagged by Ruff PLR rules)
- `PLR0912`: Too many branches — extract to functions
- `PLR6301`: No public methods — class is a namespace anti-pattern
- `PLR0206`: Descriptors in class body — use simple attributes
### Enforcement
```toml
[tool.ruff.lint.select]
select = ["E", "F", "W", "C90", "C4", "PLR0912", "PLR6301", "PLR0206"]
[tool.ruff.lint.plr]
max-returns = 4
max-locals = 8
max-args = 5
```
## 11. Structural Dependency Mapping (SDM)