@oleksandr.rudnychenko/sync_loop 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +124 -0
- package/bin/cli.js +77 -0
- package/package.json +35 -0
- package/src/init.js +365 -0
- package/src/server.js +208 -0
- package/template/.agent-loop/README.md +75 -0
- package/template/.agent-loop/feedback.md +395 -0
- package/template/.agent-loop/glossary.md +113 -0
- package/template/.agent-loop/patterns/api-standards.md +132 -0
- package/template/.agent-loop/patterns/code-patterns.md +300 -0
- package/template/.agent-loop/patterns/refactoring-workflow.md +114 -0
- package/template/.agent-loop/patterns/testing-guide.md +258 -0
- package/template/.agent-loop/patterns.md +256 -0
- package/template/.agent-loop/reasoning-kernel.md +521 -0
- package/template/.agent-loop/validate-env.md +332 -0
- package/template/.agent-loop/validate-n.md +321 -0
- package/template/AGENTS.md +157 -0
- package/template/README.md +144 -0
- package/template/bootstrap-prompt.md +37 -0
- package/template/protocol-summary.md +54 -0
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
# Domain Glossary
|
|
2
|
+
|
|
3
|
+
Canonical terminology for the project codebase. All code, comments, docs, and agent reasoning must use these terms.
|
|
4
|
+
Referenced from [patterns.md](patterns.md). Cross-references use `[→ ID]` pattern IDs from the registry.
|
|
5
|
+
|
|
6
|
+
**Use when:**
|
|
7
|
+
- Naming a new variable, field, class, or parameter
|
|
8
|
+
- Writing comments, docstrings, or documentation
|
|
9
|
+
- Interpreting domain concepts in user requests or specs
|
|
10
|
+
- Resolving ambiguity between similar terms
|
|
11
|
+
- Checking deprecated aliases before using a term in new code
|
|
12
|
+
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
## Architecture Terms
|
|
16
|
+
|
|
17
|
+
{Fill in during bootstrap with project-specific layer terms.}
|
|
18
|
+
|
|
19
|
+
| Term | Canonical Symbol | Definition |
|
|
20
|
+
|------|------------------|------------|
|
|
21
|
+
| **service** | `*Service` [→ P2, P10] | Business orchestration unit — contains domain logic and data access. No transport concerns. |
|
|
22
|
+
| **route** | `*_router` / `*_controller` [→ P5] | Transport boundary — handles requests, delegates to services. No business logic. |
|
|
23
|
+
| **adapter** | `*Adapter` [→ P1] | Concrete implementation of an infrastructure port/interface. |
|
|
24
|
+
| **config** | `Settings` / `Config` [→ P11] | Centralized configuration — single env source with startup validation. |
|
|
25
|
+
|
|
26
|
+
{Add project-specific architecture terms here.}
|
|
27
|
+
|
|
28
|
+
---
|
|
29
|
+
|
|
30
|
+
## Domain Objects
|
|
31
|
+
|
|
32
|
+
{Fill in during bootstrap with actual domain models.}
|
|
33
|
+
|
|
34
|
+
| Term | Canonical Symbol | Model / Location | Description |
|
|
35
|
+
|------|------------------|------------------|-------------|
|
|
36
|
+
| {entity} | `{ClassName}` | `{models/file.py}` | {what it represents} |
|
|
37
|
+
|
|
38
|
+
---
|
|
39
|
+
|
|
40
|
+
## Agent Loop Lifecycle Terms
|
|
41
|
+
|
|
42
|
+
| Term | Meaning |
|
|
43
|
+
|------|---------|
|
|
44
|
+
| **SENSE** | State detection and requirement extraction |
|
|
45
|
+
| **GKP** | Generated Knowledge Pack — context retrieval + compression |
|
|
46
|
+
| **DECIDE+ACT** | Mode selection and immediate implementation |
|
|
47
|
+
| **CHALLENGE-TEST** | Validation loop with retry-driven patching |
|
|
48
|
+
| **UPDATE** | State transition persistence |
|
|
49
|
+
| **LEARN** | Persisting reusable corrections and heuristics |
|
|
50
|
+
| **REPORT** | Writing non-trivial session outcomes |
|
|
51
|
+
|
|
52
|
+
---
|
|
53
|
+
|
|
54
|
+
## Naming Rules
|
|
55
|
+
|
|
56
|
+
1. **One concept → one canonical term** — never use synonyms in code
|
|
57
|
+
2. **Prefer explicit nouns** over abbreviations (`service` not `svc`)
|
|
58
|
+
3. **Keep boundary model names stable** once published to consumers
|
|
59
|
+
4. **Record deprecated aliases** before removal (see table below)
|
|
60
|
+
5. **Suffix conventions** (adapt to project):
|
|
61
|
+
- `*Service` for business logic
|
|
62
|
+
- `*Router` / `*Controller` for transport handlers
|
|
63
|
+
- `*Adapter` for infrastructure implementations
|
|
64
|
+
- `*Port` / `*Interface` for infrastructure abstractions
|
|
65
|
+
|
|
66
|
+
---
|
|
67
|
+
|
|
68
|
+
## Deprecated Alias Table
|
|
69
|
+
|
|
70
|
+
| Deprecated | Canonical | Notes |
|
|
71
|
+
|------------|-----------|-------|
|
|
72
|
+
| `svc` | `service` | Abbreviated form |
|
|
73
|
+
| `handler` | `service` | Avoid ambiguity with route handlers |
|
|
74
|
+
|
|
75
|
+
{Add project-specific deprecated aliases here.}
|
|
76
|
+
|
|
77
|
+
---
|
|
78
|
+
|
|
79
|
+
## Data Flow Diagram
|
|
80
|
+
|
|
81
|
+
{Fill in during bootstrap with actual project data flow.}
|
|
82
|
+
|
|
83
|
+
```
|
|
84
|
+
┌──────────────────────────────┐
|
|
85
|
+
│ USER INPUT │
|
|
86
|
+
├──────────────────────────────┤
|
|
87
|
+
│ Request → Transport Layer │
|
|
88
|
+
└─────────────┬────────────────┘
|
|
89
|
+
│
|
|
90
|
+
▼
|
|
91
|
+
┌──────────────────────────────┐
|
|
92
|
+
│ SERVICE LAYER │
|
|
93
|
+
├──────────────────────────────┤
|
|
94
|
+
│ Business Logic → Data Access│
|
|
95
|
+
└─────────────┬────────────────┘
|
|
96
|
+
│
|
|
97
|
+
▼
|
|
98
|
+
┌──────────────────────────────┐
|
|
99
|
+
│ OUTPUT │
|
|
100
|
+
├──────────────────────────────┤
|
|
101
|
+
│ Response → Side Effects │
|
|
102
|
+
└──────────────────────────────┘
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
---
|
|
106
|
+
|
|
107
|
+
## Related Documents
|
|
108
|
+
|
|
109
|
+
| Document | Purpose |
|
|
110
|
+
|----------|---------|
|
|
111
|
+
| [reasoning-kernel.md](reasoning-kernel.md) | Where lifecycle terms are executed |
|
|
112
|
+
| [patterns.md](patterns.md) | Where pattern IDs are defined and routed |
|
|
113
|
+
| [../AGENTS.md](../AGENTS.md) | Main entrypoint with full architecture reference |
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
# API Standards (R3)
|
|
2
|
+
|
|
3
|
+
Standards for consistent boundary contracts: HTTP routes, request/response models, error envelopes, and documentation.
|
|
4
|
+
Referenced from [../patterns.md](../patterns.md).
|
|
5
|
+
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
## Requirements
|
|
9
|
+
|
|
10
|
+
1. **Typed Models**: Every route must define explicit request and response models. No raw `dict` or untyped `JSONResponse` for success cases.
|
|
11
|
+
|
|
12
|
+
2. **Docstrings**: Every route handler must have a docstring with summary and description.
|
|
13
|
+
|
|
14
|
+
3. **Tags & Metadata**: Group routes with semantic tags. Provide `summary` / `description` in path operations.
|
|
15
|
+
|
|
16
|
+
4. **Spec Generation**: After modifying routes, regenerate API documentation (OpenAPI spec, Swagger, or equivalent).
|
|
17
|
+
|
|
18
|
+
---
|
|
19
|
+
|
|
20
|
+
## Workflow for New Routes
|
|
21
|
+
|
|
22
|
+
```
|
|
23
|
+
1. Define Request/Response models in the module's models file
|
|
24
|
+
2. Implement the route handler using those models
|
|
25
|
+
3. Register the router in the app entrypoint with appropriate tags
|
|
26
|
+
4. Run spec generation script
|
|
27
|
+
5. Verify generated docs match expectations
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
### Example Route
|
|
31
|
+
|
|
32
|
+
```python
|
|
33
|
+
# models.py — typed boundary contracts
|
|
34
|
+
@dataclass
|
|
35
|
+
class CreateEntityRequest:
|
|
36
|
+
name: str
|
|
37
|
+
entity_type: str
|
|
38
|
+
metadata: dict[str, Any] = field(default_factory=dict)
|
|
39
|
+
|
|
40
|
+
@dataclass
|
|
41
|
+
class EntityResponse:
|
|
42
|
+
id: str
|
|
43
|
+
name: str
|
|
44
|
+
entity_type: str
|
|
45
|
+
status: str
|
|
46
|
+
created_at: str
|
|
47
|
+
|
|
48
|
+
# routes.py — thin transport layer
|
|
49
|
+
@router.post("/entities", response_model=EntityResponse)
|
|
50
|
+
def create_entity(
|
|
51
|
+
data: CreateEntityRequest,
|
|
52
|
+
service: EntityService = Depends(get_service),
|
|
53
|
+
) -> EntityResponse:
|
|
54
|
+
"""Create a new entity for processing.
|
|
55
|
+
|
|
56
|
+
Validates input, delegates to service, returns created entity.
|
|
57
|
+
"""
|
|
58
|
+
result = service.create(data)
|
|
59
|
+
return EntityResponse(
|
|
60
|
+
id=result.id,
|
|
61
|
+
name=result.name,
|
|
62
|
+
entity_type=result.entity_type,
|
|
63
|
+
status=result.status,
|
|
64
|
+
created_at=result.created_at.isoformat(),
|
|
65
|
+
)
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
---
|
|
69
|
+
|
|
70
|
+
## Error Envelope
|
|
71
|
+
|
|
72
|
+
All error responses must follow a consistent structure:
|
|
73
|
+
|
|
74
|
+
```python
|
|
75
|
+
# Standard error response
|
|
76
|
+
@dataclass
|
|
77
|
+
class ErrorResponse:
|
|
78
|
+
error: str # Machine-readable error code
|
|
79
|
+
message: str # Human-readable description
|
|
80
|
+
details: dict = field(default_factory=dict) # Optional context
|
|
81
|
+
|
|
82
|
+
# Usage at route boundary
|
|
83
|
+
@router.get("/entities/{entity_id}")
|
|
84
|
+
def get_entity(entity_id: str, service = Depends(get_service)):
|
|
85
|
+
try:
|
|
86
|
+
return service.get(entity_id)
|
|
87
|
+
except NotFoundError as exc:
|
|
88
|
+
raise HTTPException(status_code=404, detail=str(exc))
|
|
89
|
+
except ValidationError as exc:
|
|
90
|
+
raise HTTPException(status_code=400, detail=str(exc))
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
---
|
|
94
|
+
|
|
95
|
+
## Documentation Rules
|
|
96
|
+
|
|
97
|
+
| Rule | Detail |
|
|
98
|
+
|------|--------|
|
|
99
|
+
| Document each endpoint intent | What does it do, who calls it |
|
|
100
|
+
| Define validation constraints | Required fields, ranges, formats |
|
|
101
|
+
| Include success + error examples | Show typical 200, 400, 404, 500 |
|
|
102
|
+
| Keep schema docs in sync | Regenerate after every route change |
|
|
103
|
+
|
|
104
|
+
---
|
|
105
|
+
|
|
106
|
+
## Change Safety
|
|
107
|
+
|
|
108
|
+
| Change Type | Requirements |
|
|
109
|
+
|-------------|-------------|
|
|
110
|
+
| **Additive** (new field, new endpoint) | Must be backward-compatible; new fields optional with defaults |
|
|
111
|
+
| **Modification** (rename field, change type) | Requires migration notes + caller updates + NEIGHBOR validation |
|
|
112
|
+
| **Removal** (delete field, remove endpoint) | Requires deprecation period, caller audit, explicit approval |
|
|
113
|
+
|
|
114
|
+
---
|
|
115
|
+
|
|
116
|
+
## Versioning Strategy
|
|
117
|
+
|
|
118
|
+
- Prefer additive changes over breaking changes
|
|
119
|
+
- When breaking changes are unavoidable:
|
|
120
|
+
1. Document the change in migration notes
|
|
121
|
+
2. Update all known consumers
|
|
122
|
+
3. Run NEIGHBOR validation to check boundary contracts
|
|
123
|
+
4. Consider a version prefix if multiple consumers exist
|
|
124
|
+
|
|
125
|
+
---
|
|
126
|
+
|
|
127
|
+
## Related Documents
|
|
128
|
+
|
|
129
|
+
| Document | Purpose |
|
|
130
|
+
|----------|---------|
|
|
131
|
+
| [../validate-n.md](../validate-n.md) | Contract compatibility checks (NEIGHBOR) |
|
|
132
|
+
| [code-patterns.md](code-patterns.md) | P5 (Transport Route), P6 (Typed Models) |
|
|
@@ -0,0 +1,300 @@
|
|
|
1
|
+
# Code Patterns (P1–P11)
|
|
2
|
+
|
|
3
|
+
Reusable implementation patterns for layered application code.
|
|
4
|
+
Referenced from [../patterns.md](../patterns.md).
|
|
5
|
+
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
## P1 · Port/Adapter
|
|
9
|
+
|
|
10
|
+
Abstracts infrastructure behind protocol interfaces. Decouples domain logic from external systems.
|
|
11
|
+
|
|
12
|
+
```python
|
|
13
|
+
# Port (interface/protocol)
|
|
14
|
+
class StoragePort(Protocol):
|
|
15
|
+
def search(
|
|
16
|
+
self,
|
|
17
|
+
collection: str,
|
|
18
|
+
query: str,
|
|
19
|
+
*,
|
|
20
|
+
filters: dict | None = None,
|
|
21
|
+
limit: int = 10,
|
|
22
|
+
) -> list[Record]: ...
|
|
23
|
+
|
|
24
|
+
# Adapter (concrete implementation)
|
|
25
|
+
class DatabaseAdapter:
|
|
26
|
+
def __init__(self, client: DBClient) -> None:
|
|
27
|
+
self._client = client
|
|
28
|
+
|
|
29
|
+
def search(
|
|
30
|
+
self,
|
|
31
|
+
collection: str,
|
|
32
|
+
query: str,
|
|
33
|
+
*,
|
|
34
|
+
filters: dict | None = None,
|
|
35
|
+
limit: int = 10,
|
|
36
|
+
) -> list[Record]:
|
|
37
|
+
# Implementation against real infrastructure
|
|
38
|
+
...
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
**Key rules:**
|
|
42
|
+
- Port lives in `libs/{component}/port.*`
|
|
43
|
+
- Adapter lives in `libs/{component}/{impl}.*`
|
|
44
|
+
- Services depend on port interfaces, not adapters directly
|
|
45
|
+
|
|
46
|
+
---
|
|
47
|
+
|
|
48
|
+
## P2 · Domain Module
|
|
49
|
+
|
|
50
|
+
Each domain module follows a consistent multi-file layout:
|
|
51
|
+
|
|
52
|
+
| File | Purpose |
|
|
53
|
+
|------|---------|
|
|
54
|
+
| `models.*` | Domain entities and value objects |
|
|
55
|
+
| `services.*` | Business logic and orchestration |
|
|
56
|
+
| `routes.*` | Transport endpoints |
|
|
57
|
+
| `tasks.*` | Background tasks (if async processing needed) |
|
|
58
|
+
|
|
59
|
+
```python
|
|
60
|
+
# services.py — business logic only, no transport concerns
|
|
61
|
+
class OrderService:
|
|
62
|
+
def __init__(self, repository: OrderRepository) -> None:
|
|
63
|
+
self._repository = repository
|
|
64
|
+
|
|
65
|
+
def process(self, *, order_id: str) -> ProcessResult:
|
|
66
|
+
order = self._repository.get(order_id)
|
|
67
|
+
# business logic here
|
|
68
|
+
return ProcessResult(order_id=order.id, status="completed")
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
---
|
|
72
|
+
|
|
73
|
+
## P3 · Background Task Boundary
|
|
74
|
+
|
|
75
|
+
Task handlers stay thin. Business logic always lives in services.
|
|
76
|
+
|
|
77
|
+
```python
|
|
78
|
+
# tasks.py — thin wrapper, delegates to service
|
|
79
|
+
def process_order_task(runtime: TaskRuntime, order_id: str):
|
|
80
|
+
"""Background task that delegates to service."""
|
|
81
|
+
service = runtime.order_service
|
|
82
|
+
service.update_status(order_id, status="processing")
|
|
83
|
+
|
|
84
|
+
try:
|
|
85
|
+
service.process(order_id=order_id)
|
|
86
|
+
service.update_status(order_id, status="completed")
|
|
87
|
+
except Exception as exc:
|
|
88
|
+
service.update_status(order_id, status="failed", error=str(exc))
|
|
89
|
+
raise
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
**Key rules:**
|
|
93
|
+
- Tasks never contain business logic
|
|
94
|
+
- Dependencies injected via runtime, not imported directly
|
|
95
|
+
- Always update status on success and failure
|
|
96
|
+
|
|
97
|
+
---
|
|
98
|
+
|
|
99
|
+
## P4 · App Context / Composition Root
|
|
100
|
+
|
|
101
|
+
Centralized dependency wiring, initialized once at startup:
|
|
102
|
+
|
|
103
|
+
```python
|
|
104
|
+
@dataclass
|
|
105
|
+
class AppContext:
|
|
106
|
+
config: Config
|
|
107
|
+
session_factory: SessionFactory
|
|
108
|
+
services: ServiceRegistry
|
|
109
|
+
logger: Logger
|
|
110
|
+
|
|
111
|
+
_context: AppContext | None = None
|
|
112
|
+
|
|
113
|
+
def init_app_context(config: Config) -> AppContext:
|
|
114
|
+
global _context
|
|
115
|
+
if _context:
|
|
116
|
+
return _context
|
|
117
|
+
_context = AppContext(config=config, ...)
|
|
118
|
+
return _context
|
|
119
|
+
|
|
120
|
+
def get_app_context() -> AppContext:
|
|
121
|
+
if _context is None:
|
|
122
|
+
return init_app_context(load_config())
|
|
123
|
+
return _context
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
---
|
|
127
|
+
|
|
128
|
+
## P5 · Transport Route
|
|
129
|
+
|
|
130
|
+
Routes only handle transport concerns; all logic delegated to services:
|
|
131
|
+
|
|
132
|
+
```python
|
|
133
|
+
router = APIRouter()
|
|
134
|
+
|
|
135
|
+
def get_service() -> OrderService:
|
|
136
|
+
ctx = get_app_context()
|
|
137
|
+
return OrderService(ctx.repository)
|
|
138
|
+
|
|
139
|
+
@router.post("/orders")
|
|
140
|
+
def create_order(
|
|
141
|
+
data: CreateOrderRequest,
|
|
142
|
+
service: OrderService = Depends(get_service),
|
|
143
|
+
) -> OrderResponse:
|
|
144
|
+
result = service.create(data)
|
|
145
|
+
return OrderResponse(id=result.id, status=result.status)
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
---
|
|
149
|
+
|
|
150
|
+
## P6 · Typed Models
|
|
151
|
+
|
|
152
|
+
Domain entities with explicit types and serialization:
|
|
153
|
+
|
|
154
|
+
```python
|
|
155
|
+
@dataclass(slots=True)
|
|
156
|
+
class OrderItem:
|
|
157
|
+
product_id: str
|
|
158
|
+
quantity: int
|
|
159
|
+
unit_price: float
|
|
160
|
+
tags: list[str] = field(default_factory=list)
|
|
161
|
+
|
|
162
|
+
def to_dict(self) -> dict[str, Any]:
|
|
163
|
+
return {
|
|
164
|
+
"product_id": self.product_id,
|
|
165
|
+
"quantity": self.quantity,
|
|
166
|
+
"unit_price": self.unit_price,
|
|
167
|
+
"tags": self.tags,
|
|
168
|
+
}
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
---
|
|
172
|
+
|
|
173
|
+
## P7 · Collection/Enum Safety
|
|
174
|
+
|
|
175
|
+
Replace magic strings with typed enums:
|
|
176
|
+
|
|
177
|
+
```python
|
|
178
|
+
class Collection(str, Enum):
|
|
179
|
+
ORDERS = "orders"
|
|
180
|
+
PRODUCTS = "products"
|
|
181
|
+
USERS = "users"
|
|
182
|
+
|
|
183
|
+
# Usage: repository.query(Collection.ORDERS, ...)
|
|
184
|
+
# NOT: repository.query("orders", ...)
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
---
|
|
188
|
+
|
|
189
|
+
## P8 · Error Handling
|
|
190
|
+
|
|
191
|
+
Layered exception hierarchy with boundary translation:
|
|
192
|
+
|
|
193
|
+
```python
|
|
194
|
+
# Domain exceptions
|
|
195
|
+
class DomainError(Exception):
|
|
196
|
+
"""Base error for domain."""
|
|
197
|
+
|
|
198
|
+
class NotFoundError(DomainError):
|
|
199
|
+
"""Resource not found."""
|
|
200
|
+
|
|
201
|
+
class ValidationError(DomainError):
|
|
202
|
+
"""Invalid input or state."""
|
|
203
|
+
|
|
204
|
+
# Route-level translation
|
|
205
|
+
@router.get("/orders/{order_id}")
|
|
206
|
+
def get_order(order_id: str, service = Depends(get_service)):
|
|
207
|
+
try:
|
|
208
|
+
return service.get(order_id)
|
|
209
|
+
except NotFoundError as exc:
|
|
210
|
+
raise HTTPException(status_code=404, detail=str(exc))
|
|
211
|
+
except ValidationError as exc:
|
|
212
|
+
raise HTTPException(status_code=400, detail=str(exc))
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
---
|
|
216
|
+
|
|
217
|
+
## P9 · Type Hints Everywhere
|
|
218
|
+
|
|
219
|
+
All code must have complete type annotations:
|
|
220
|
+
|
|
221
|
+
```python
|
|
222
|
+
# ✅ Good — fully typed
|
|
223
|
+
def process(
|
|
224
|
+
order_id: str,
|
|
225
|
+
*,
|
|
226
|
+
callback: Callable[..., Awaitable[Response]] | None = None,
|
|
227
|
+
) -> tuple[str, dict[str, Any]] | None:
|
|
228
|
+
...
|
|
229
|
+
|
|
230
|
+
# ❌ Bad — missing annotations
|
|
231
|
+
def process(order_id, callback=None):
|
|
232
|
+
...
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
**Common type aliases:**
|
|
236
|
+
```python
|
|
237
|
+
SessionFactory = Callable[[], Session]
|
|
238
|
+
Filters = Mapping[str, Any]
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
---
|
|
242
|
+
|
|
243
|
+
## P10 · Service Orchestration
|
|
244
|
+
|
|
245
|
+
Services accept all dependencies via constructor — no hidden state:
|
|
246
|
+
|
|
247
|
+
```python
|
|
248
|
+
# Production code
|
|
249
|
+
class AnalysisService:
|
|
250
|
+
def __init__(
|
|
251
|
+
self,
|
|
252
|
+
repository: Repository,
|
|
253
|
+
evaluator: EvaluationService,
|
|
254
|
+
):
|
|
255
|
+
self._repository = repository
|
|
256
|
+
self._evaluator = evaluator
|
|
257
|
+
|
|
258
|
+
# Test code — inject mocks
|
|
259
|
+
service = AnalysisService(
|
|
260
|
+
repository=mock_repository,
|
|
261
|
+
evaluator=mock_evaluator,
|
|
262
|
+
)
|
|
263
|
+
```
|
|
264
|
+
|
|
265
|
+
---
|
|
266
|
+
|
|
267
|
+
## P11 · Config Isolation
|
|
268
|
+
|
|
269
|
+
Centralized, environment-based configuration with startup validation:
|
|
270
|
+
|
|
271
|
+
```python
|
|
272
|
+
@dataclass
|
|
273
|
+
class Config:
|
|
274
|
+
database_url: str
|
|
275
|
+
debug: bool = False
|
|
276
|
+
max_workers: int = 4
|
|
277
|
+
|
|
278
|
+
@classmethod
|
|
279
|
+
def from_env(cls) -> "Config":
|
|
280
|
+
return cls(
|
|
281
|
+
database_url=os.environ["DATABASE_URL"],
|
|
282
|
+
debug=os.environ.get("DEBUG", "0") == "1",
|
|
283
|
+
max_workers=int(os.environ.get("MAX_WORKERS", "4")),
|
|
284
|
+
)
|
|
285
|
+
```
|
|
286
|
+
|
|
287
|
+
**Key rules:**
|
|
288
|
+
- All config read from environment at startup
|
|
289
|
+
- No scattered `os.environ` calls inside business logic
|
|
290
|
+
- Test config overrides controlled via fixtures
|
|
291
|
+
|
|
292
|
+
---
|
|
293
|
+
|
|
294
|
+
## Related Documents
|
|
295
|
+
|
|
296
|
+
| Document | Purpose |
|
|
297
|
+
|----------|---------|
|
|
298
|
+
| [../patterns.md](../patterns.md) | Pattern routing index |
|
|
299
|
+
| [refactoring-workflow.md](refactoring-workflow.md) | Safe structural changes |
|
|
300
|
+
| [testing-guide.md](testing-guide.md) | Verification strategy |
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
# Refactoring Workflow (R1)
|
|
2
|
+
|
|
3
|
+
Checklist and approach for moving files, changing imports, or restructuring modules.
|
|
4
|
+
Referenced from [../patterns.md](../patterns.md).
|
|
5
|
+
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
## Refactoring Checklist
|
|
9
|
+
|
|
10
|
+
```
|
|
11
|
+
Phase 1: PLAN
|
|
12
|
+
☐ Identify all files to move/rename/extract
|
|
13
|
+
☐ Map old imports → new imports
|
|
14
|
+
☐ Check for documentation references (README, docstrings, agent-loop specs)
|
|
15
|
+
☐ Identify public interfaces that must remain stable
|
|
16
|
+
|
|
17
|
+
Phase 2: EXECUTE
|
|
18
|
+
☐ Move files
|
|
19
|
+
☐ Update imports in moved files (internal references)
|
|
20
|
+
☐ Update all caller imports (use grep to find every reference)
|
|
21
|
+
☐ Update documentation examples if they contain import paths
|
|
22
|
+
☐ Update .agent-loop/patterns.md structure section if layout changed
|
|
23
|
+
|
|
24
|
+
Phase 3: VALIDATE (NON-NEGOTIABLE)
|
|
25
|
+
☐ 1. Type check: run project type checker
|
|
26
|
+
☐ 2. Run tests: execute test suite with fail-fast
|
|
27
|
+
☐ 3. Check docs: grep for old import paths across all files
|
|
28
|
+
☐ 4. Verify no orphaned imports (unused or broken)
|
|
29
|
+
|
|
30
|
+
Phase 4: DOCUMENT
|
|
31
|
+
☐ Update README structure section
|
|
32
|
+
☐ Update architecture docs if needed
|
|
33
|
+
☐ Create report in docs/reports/ if the refactor is major
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
---
|
|
37
|
+
|
|
38
|
+
## Example: Moving a Module File
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
# 1. Move file
|
|
42
|
+
mv src/modules/old_location/processor.py src/modules/new_location/processor.py
|
|
43
|
+
|
|
44
|
+
# 2. Update imports inside the moved file (internal references)
|
|
45
|
+
# e.g., relative imports that changed due to new directory depth
|
|
46
|
+
|
|
47
|
+
# 3. Find ALL references to the old path
|
|
48
|
+
grep -r "from src.modules.old_location.processor" .
|
|
49
|
+
grep -r "import old_location.processor" .
|
|
50
|
+
|
|
51
|
+
# 4. Update each reference found:
|
|
52
|
+
# - tests/unit/test_processor.py
|
|
53
|
+
# - src/modules/new_location/tasks.py
|
|
54
|
+
# - docs/architecture.md (if it mentions import paths)
|
|
55
|
+
|
|
56
|
+
# 5. MANDATORY: Run validation
|
|
57
|
+
# Type check → Tests → Grep for leftover old paths
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
**Why tests after docs?** Documentation often contains import examples. Tests verify imports actually work and catch typos in updated paths.
|
|
61
|
+
|
|
62
|
+
---
|
|
63
|
+
|
|
64
|
+
## Example: Extracting a Function to a New Module
|
|
65
|
+
|
|
66
|
+
```bash
|
|
67
|
+
# 1. Create the new module
|
|
68
|
+
touch src/libs/parsing/helpers.py
|
|
69
|
+
|
|
70
|
+
# 2. Move the function definition to the new file
|
|
71
|
+
# Update its internal imports
|
|
72
|
+
|
|
73
|
+
# 3. In the original file, replace the function body with an import:
|
|
74
|
+
# from src.libs.parsing.helpers import parse_response
|
|
75
|
+
|
|
76
|
+
# 4. Find all OTHER callers of the original location
|
|
77
|
+
grep -r "from src.modules.analysis.utils import parse_response" .
|
|
78
|
+
|
|
79
|
+
# 5. Update all callers to use the new import path
|
|
80
|
+
|
|
81
|
+
# 6. VALIDATE: type check + tests + grep for old path
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
---
|
|
85
|
+
|
|
86
|
+
## Guardrails
|
|
87
|
+
|
|
88
|
+
- **Prefer reversible steps** — move one file at a time, validate, then move next
|
|
89
|
+
- **Never combine unrelated refactors** — one logical change per commit
|
|
90
|
+
- **Do not hide breaking API changes** — if a public interface moved, update all consumers
|
|
91
|
+
- **Never skip Phase 3** — validation is non-negotiable, even for "simple" moves
|
|
92
|
+
- **Grep is your friend** — always search for the old path after moving; IDEs miss things
|
|
93
|
+
|
|
94
|
+
---
|
|
95
|
+
|
|
96
|
+
## Common Pitfalls
|
|
97
|
+
|
|
98
|
+
| Pitfall | Prevention |
|
|
99
|
+
|---------|------------|
|
|
100
|
+
| Circular imports after move | Map dependency graph before moving |
|
|
101
|
+
| Tests pass but type checker fails | Always run type checker first |
|
|
102
|
+
| Docs reference old paths | Grep docs/ and *.md files explicitly |
|
|
103
|
+
| Forgot to update __init__.py re-exports | Check all `__init__.py` files in affected packages |
|
|
104
|
+
| Moved too many files at once | Move one file → validate → repeat |
|
|
105
|
+
|
|
106
|
+
---
|
|
107
|
+
|
|
108
|
+
## Related Documents
|
|
109
|
+
|
|
110
|
+
| Document | Purpose |
|
|
111
|
+
|----------|---------|
|
|
112
|
+
| [../validate-env.md](../validate-env.md) | Stage 1 gates (type check, tests) |
|
|
113
|
+
| [../validate-n.md](../validate-n.md) | Stage 2 neighbor checks (boundary impact) |
|
|
114
|
+
| [testing-guide.md](testing-guide.md) | Regression test strategy |
|