Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .opencode/agents/product-owner.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,4 +59,5 @@ When a gap is reported (by software-engineer or reviewer):
## Available Skills

- `session-workflow` — session start/end protocol
- `feature-selection` — when TODO.md is idle: score and select next backlog feature using WSJF
- `scope` — Step 1: 3-session discovery (Phase 1 + 2), stories (Phase 3), and criteria (Phase 4)
5 changes: 3 additions & 2 deletions .opencode/agents/software-engineer.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ Load `skill session-workflow` first — it reads TODO.md, orients you to the cur
| Step | Action |
|---|---|
| **Step 2 — ARCH** | Load `skill implementation` — contains Step 2 architecture protocol |
| **Step 3 — TDD LOOP** | Load `skill implementation` — contains Step 3 TDD Loop |
| **Step 3 — TDD LOOP** | Load `skill implementation` — contains Step 3 TDD Loop; load `skill refactor` when entering REFACTOR phase or doing preparatory refactoring |
| **Step 5 — after PO accepts** | Load `skill pr-management` and `skill git-release` as needed |

## Ownership Rules
Expand All @@ -57,7 +57,8 @@ If during implementation you discover behavior not covered by existing acceptanc

- `session-workflow` — session start/end protocol
- `implementation` — Steps 2-3: architecture + TDD loop
- `design-patterns` — on-demand when smell detected during refactor
- `refactor` — REFACTOR phase and preparatory refactoring (load on-demand)
- `design-patterns` — on-demand when smell detected during architecture or refactor
- `pr-management` — Step 5: PRs with conventional commits
- `git-release` — Step 5: calver versioning and themed release naming
- `create-skill` — meta: create new skills when needed
3 changes: 2 additions & 1 deletion .opencode/skills/create-skill/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ Before writing any skill, research the domain to ground the skill in industry st
- Vendor documentation (OpenAI, Anthropic, Google, Microsoft)
- Industry standards (ISO, NIST, OMG)
- Established methodologies (e.g., FDD, Scrum, Kanban for process skills)
3. **Read existing research**: Check `docs/academic_research.md` for related entries
3. **Read existing research**: Check `docs/scientific-research/` for related entries — each file covers a domain (testing, oop-design, architecture, ai-agents, etc.)
4. **Synthesize conclusions**: Extract actionable conclusions — what works, why, and when to apply it
5. **Embed as guidance**: Write the skill's steps, checklists, and decision rules based on those conclusions — not as academic citations but as direct guidance ("Use X because it produces Y outcome")

Expand Down Expand Up @@ -133,6 +133,7 @@ Add the skill name to the agent's "Available Skills" section so the agent knows
| Skill | Used By | Purpose |
|---|---|---|
| `session-workflow` | all agents | Session start/end protocol |
| `feature-selection` | product-owner | Score and select next backlog feature (WSJF) |
| `scope` | product-owner | Step 1: define acceptance criteria |
| `implementation` | software-engineer | Steps 2-3: architecture + TDD loop |
| `design-patterns` | software-engineer | Steps 2, 3: refactor when smell detected |
Expand Down
199 changes: 11 additions & 188 deletions .opencode/skills/design-patterns/SKILL.md
Original file line number Diff line number Diff line change
@@ -1,26 +1,26 @@
---
name: design-patterns
description: Reference skill for GoF design patterns, SOLID, Object Calisthenics, Python Zen, and other SE principles — with smell triggers and Python before/after examples
version: "1.0"
description: GoF design pattern catalogue — smell triggers and Python before/after examples
version: "2.0"
author: software-engineer
audience: software-engineer
workflow: feature-lifecycle
---

# Design Patterns Reference

Load this skill when:
- Running the architecture smell check in Step 2 and a smell is detected
- Refactoring in Step 3 and a pattern smell appears in the self-declaration
Load this skill when the refactor skill's smell table points to a GoF pattern and you need the Python before/after example.

Sources: Gamma, Helm, Johnson, Vlissides. *Design Patterns: Elements of Reusable Object-Oriented Software*. Addison-Wesley, 1995. See `docs/scientific-research/oop-design.md` entry 34.

---

## How to Use This Skill

1. **Identify the smell** from the checklist in your self-declaration or architecture check
1. **Identify the smell** from the refactor skill's lookup table
2. **Find the smell category** below (Creational / Structural / Behavioral)
3. **Read the trigger and the before/after example**
4. **Apply the pattern** and update the Architecture section (Step 2) or the refactored code (Step 3)
4. **Apply the pattern** update the stub files (Step 2) or the refactored code (Step 3)

---

Expand Down Expand Up @@ -178,7 +178,7 @@ def apply_discount(order: Order, strategy: DiscountStrategy) -> Money:
#### Smell: Feature Envy
**Signal**: A method in class A uses data from class B more than its own data. The method "envies" class B.

**Pattern**: Move Method to the envied class (not a GoF pattern — a Fowler refactoring that often precedes Strategy or Command)
**Pattern**: Move Method to the envied class (Fowler refactoring that often precedes Strategy or Command)

```python
# BEFORE — OrderPrinter knows too much about Order internals
Expand Down Expand Up @@ -309,9 +309,9 @@ class Order:
class Order:
def confirm(self) -> None:
self.status = "confirmed"
EmailService().send_confirmation(self) # direct coupling
InventoryService().reserve(self) # direct coupling
AnalyticsService().record_conversion(self) # direct coupling
EmailService().send_confirmation(self) # direct coupling
InventoryService().reserve(self) # direct coupling
AnalyticsService().record_conversion(self) # direct coupling
```

```python
Expand Down Expand Up @@ -380,182 +380,6 @@ class JsonImporter(Importer):

---

## SOLID — Python Examples

### S — Single Responsibility
One class, one reason to change.

```python
# WRONG — Report handles both data and formatting
class Report:
def generate(self) -> dict: ...
def to_pdf(self) -> bytes: ... # separate concern
def to_csv(self) -> str: ... # separate concern

# RIGHT — split concerns
class Report:
def generate(self) -> ReportData: ...

class PdfRenderer:
def render(self, data: ReportData) -> bytes: ...
```

### O — Open/Closed
Open for extension, closed for modification.

```python
# WRONG — must edit this function to add a new format
def export(data: ReportData, fmt: str) -> bytes:
if fmt == "pdf": ...
elif fmt == "csv": ...

# RIGHT — new formats extend without touching existing code
class Exporter(Protocol):
def export(self, data: ReportData) -> bytes: ...

class PdfExporter:
def export(self, data: ReportData) -> bytes: ...
```

### L — Liskov Substitution
Subtypes must be fully substitutable for their base type.

```python
# WRONG — ReadOnlyFile violates the contract of File
class File:
def write(self, content: str) -> None: ...

class ReadOnlyFile(File):
def write(self, content: str) -> None:
raise PermissionError # narrows the contract — LSP violation

# RIGHT — separate interfaces for readable and writable
class ReadableFile(Protocol):
def read(self) -> str: ...

class WritableFile(Protocol):
def write(self, content: str) -> None: ...
```

### I — Interface Segregation
No implementor should be forced to implement methods it doesn't use.

```python
# WRONG — Printer is forced to implement scan() and fax()
class Machine(Protocol):
def print(self, doc: Document) -> None: ...
def scan(self, doc: Document) -> None: ...
def fax(self, doc: Document) -> None: ...

# RIGHT — each capability is its own Protocol
class Printer(Protocol):
def print(self, doc: Document) -> None: ...

class Scanner(Protocol):
def scan(self, doc: Document) -> None: ...
```

### D — Dependency Inversion
Domain depends on abstractions (Protocols), not on concrete I/O or frameworks.

```python
# WRONG — domain imports infrastructure directly
from app.db import PostgresConnection

class OrderRepository:
def __init__(self) -> None:
self.db = PostgresConnection() # domain imports infra

# RIGHT — domain defines the Protocol; infra implements it
class OrderRepository(Protocol):
def find(self, order_id: OrderId) -> Order: ...
def save(self, order: Order) -> None: ...

class PostgresOrderRepository: # in adapters/
def find(self, order_id: OrderId) -> Order: ...
def save(self, order: Order) -> None: ...
```

---

## Object Calisthenics — Python Rules

Jeff Bay's 9 rules for object-oriented discipline. Each has a Python signal.

| Rule | Constraint | Python Signal of Violation |
|---|---|---|
| **OC-1** | One indent level per method | `for` inside `if` inside a method body |
| **OC-2** | No `else` after `return` | `if cond: return x \n else: return y` |
| **OC-3** | Wrap all primitives that have domain meaning | `def process(user_id: int)` instead of `def process(user_id: UserId)` |
| **OC-4** | Wrap all collections that have domain meaning | `list[Order]` passed around instead of `OrderCollection` |
| **OC-5** | One dot per line | `obj.repo.find(id).name` |
| **OC-6** | No abbreviations | `usr`, `mgr`, `cfg`, `val`, `tmp` |
| **OC-7** | Keep classes small (≤50 lines) and methods short (≤20 lines) | Any method requiring scrolling |
| **OC-8** | No class with more than 2 instance variables | `__init__` with 3+ `self.x =` assignments |
| **OC-9** | No getters/setters | `def get_name(self)` / `def set_name(self, v)` |

---

## Python Zen — Mapped to Code Practices

The relevant items from PEP 20 (`import this`) with concrete code implications:

| Zen Item | Code Practice |
|---|---|
| Beautiful is better than ugly | Name things clearly; prefer named types over bare primitives |
| Explicit is better than implicit | Explicit return types; explicit Protocol dependencies; no magic |
| Simple is better than complex | KISS — one function, one job; prefer a plain function over a class |
| Complex is better than complicated | A well-designed abstraction is acceptable; an accidental tangle is not |
| Flat is better than nested | OC-1 — one indent level; early returns |
| Sparse is better than dense | One statement per line; no semicolons; no lambda chains |
| Readability counts | OC-6 — no abbreviations; docstrings on every public function |
| Special cases aren't special enough to break the rules | Do not add `if isinstance` branches to avoid refactoring |
| Errors should never pass silently | No bare `except:`; no `except Exception: pass` |
| In the face of ambiguity, refuse the temptation to guess | Raise on invalid input; never silently return a default |
| There should be one obvious way to do it | DRY — every shared concept in exactly one place |
| If the implementation is hard to explain, it's a bad idea | KISS — if you can't describe the function in one sentence, split it |

---

## Other Principles

### Law of Demeter (Tell, Don't Ask)
A method should only call methods on:
- `self`
- Objects passed as parameters
- Objects it creates
- Direct component objects (`self.x`)

**Violation signal**: `a.b.c()` — two dots. Assign `b = a.b` and call `b.c()`, or better: ask `a` to do what you need (`a.do_thing()`).

### Command-Query Separation (CQS)
A method either **changes state** (command) or **returns a value** (query) — never both.

```python
# WRONG — pop() both returns and mutates
value = stack.pop()

# RIGHT (CQS strict)
value = stack.peek() # query — no mutation
stack.remove_top() # command — no return value
```

Note: Python's standard library violates CQS in places (`list.pop()`, `dict.update()`). Apply CQS to your domain objects; do not fight the stdlib.

### Tell, Don't Ask
Instead of querying an object's state and acting on it externally, tell the object to do the work itself.

```python
# WRONG — ask state, decide externally
if order.status == OrderStatus.PENDING:
order.status = OrderStatus.CONFIRMED

# RIGHT — tell the object
order.confirm() # Order decides if the transition is valid
```

---

## Quick Smell → Pattern Lookup

| Smell | Pattern |
Expand All @@ -569,4 +393,3 @@ order.confirm() # Order decides if the transition is valid
| Class directly calls B, C, D on state change | Observer |
| Two functions share the same skeleton, differ in one step | Template Method |
| Subsystem is complex and callers need a simple entry point | Facade |
| Object needs logging/caching without changing its class | Decorator / Proxy |
Loading
Loading