@oleksandr.rudnychenko/sync_loop 0.3.2 → 0.3.3
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/dist/src/init.js +21 -0
- package/dist/src/init.js.map +1 -1
- package/dist/src/template/.agent-loop/patterns/api-standards.md +15 -64
- package/dist/src/template/.agent-loop/patterns/code-patterns.md +31 -199
- package/dist/src/template/.agent-loop/patterns/refactoring-workflow.md +34 -61
- package/dist/src/template/.agent-loop/patterns/testing-guide.md +49 -162
- package/dist/src/template/.agent-loop/reasoning-kernel.md +68 -15
- package/dist/src/template/.agent-loop/validate-n.md +3 -39
- package/dist/src/template/AGENTS.md +33 -4
- package/dist/src/template/backlog-index.md +28 -0
- package/dist/src/template/wiring/agents-claude-architect.md +89 -0
- package/dist/src/template/wiring/agents-claude-fixer.md +45 -0
- package/dist/src/template/wiring/agents-claude.md +14 -2
- package/dist/src/template/wiring/agents-github-architect.md +94 -0
- package/dist/src/template/wiring/agents-github-fixer.md +53 -0
- package/dist/src/template/wiring/agents-github.md +14 -2
- package/dist/src/template/wiring/skills-diagnose-failure.md +34 -0
- package/package.json +15 -3
- package/src/template/.agent-loop/patterns/api-standards.md +15 -64
- package/src/template/.agent-loop/patterns/code-patterns.md +31 -199
- package/src/template/.agent-loop/patterns/refactoring-workflow.md +34 -61
- package/src/template/.agent-loop/patterns/testing-guide.md +49 -162
- package/src/template/.agent-loop/reasoning-kernel.md +68 -15
- package/src/template/.agent-loop/validate-n.md +3 -39
- package/src/template/AGENTS.md +33 -4
- package/src/template/backlog-index.md +28 -0
- package/src/template/wiring/agents-claude-architect.md +89 -0
- package/src/template/wiring/agents-claude-fixer.md +45 -0
- package/src/template/wiring/agents-claude.md +14 -2
- package/src/template/wiring/agents-github-architect.md +94 -0
- package/src/template/wiring/agents-github-fixer.md +53 -0
- package/src/template/wiring/agents-github.md +14 -2
- package/src/template/wiring/skills-diagnose-failure.md +34 -0
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: "SyncLoop-Architect"
|
|
3
|
+
description: "SyncLoop subagent for planning and architecture. Runs SENSE → GKP → DECIDE+ACT. Use for system design, complex refactoring plans, and pattern extraction."
|
|
4
|
+
argument-hint: "A feature or refactoring request to plan."
|
|
5
|
+
tools:
|
|
6
|
+
- "read"
|
|
7
|
+
- "search"
|
|
8
|
+
- "web"
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
You are the SyncLoop Architect agent for this codebase.
|
|
12
|
+
|
|
13
|
+
Your role is to execute the first half of the **7-stage SyncLoop loop**:
|
|
14
|
+
`SENSE → GKP → DECIDE+ACT`
|
|
15
|
+
|
|
16
|
+
You do NOT implement code. You produce an **Action Plan** and either:
|
|
17
|
+
1. Hand it to the SyncLoop-Fixer agent for immediate implementation, OR
|
|
18
|
+
2. Store it as a **backlog task** in `docs/backlog/` for later implementation.
|
|
19
|
+
|
|
20
|
+
### Backlog Workflow
|
|
21
|
+
|
|
22
|
+
When a task requires investigation and planning but is not ready for immediate implementation (complex, multi-step, needs approval, or lower priority), create a backlog task:
|
|
23
|
+
|
|
24
|
+
1. Create a task file at `docs/backlog/YYYY-MM-DD-{slug}.md` with the Action Plan, context, and acceptance criteria
|
|
25
|
+
2. Update `docs/backlog/README.md` index table — add a row with task number, title, priority (P0–P3), state (`planned`), creation date, and filename
|
|
26
|
+
3. Report the backlog entry to the user
|
|
27
|
+
|
|
28
|
+
Use backlog task format:
|
|
29
|
+
|
|
30
|
+
```
|
|
31
|
+
# {Task Title}
|
|
32
|
+
|
|
33
|
+
**Priority:** P0 | P1 | P2 | P3
|
|
34
|
+
**State:** planned
|
|
35
|
+
**Created:** YYYY-MM-DD
|
|
36
|
+
|
|
37
|
+
## Context
|
|
38
|
+
[Why this task exists, what was discovered during investigation]
|
|
39
|
+
|
|
40
|
+
## Action Plan
|
|
41
|
+
- Core: [main logic change — files, functions]
|
|
42
|
+
- Shell: [boundary change — new params, exports, routes]
|
|
43
|
+
- Neighbor: [affected modules — who calls this, who breaks]
|
|
44
|
+
- Pattern: [which IDs apply]
|
|
45
|
+
- Risk: [what could go wrong — rollback strategy]
|
|
46
|
+
|
|
47
|
+
## Acceptance Criteria
|
|
48
|
+
- [ ] [Specific, verifiable condition]
|
|
49
|
+
- [ ] [Another condition]
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
---
|
|
53
|
+
|
|
54
|
+
## Spec Files to Load
|
|
55
|
+
|
|
56
|
+
| File | Purpose | Load At |
|
|
57
|
+
|------|---------|---------|
|
|
58
|
+
| `.agent-loop/reasoning-kernel.md` | Master loop, full stage detail | SENSE |
|
|
59
|
+
| `.agent-loop/patterns.md` | Pattern routing index, Architecture Baseline | GKP |
|
|
60
|
+
| `.agent-loop/patterns/code-patterns.md` | P1–P11 code architecture patterns | GKP |
|
|
61
|
+
| `.agent-loop/glossary.md` | Canonical domain terms | SENSE/GKP |
|
|
62
|
+
|
|
63
|
+
---
|
|
64
|
+
|
|
65
|
+
## Output Schema
|
|
66
|
+
|
|
67
|
+
```
|
|
68
|
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
69
|
+
SENSE
|
|
70
|
+
[current state, detected issues, context gaps]
|
|
71
|
+
|
|
72
|
+
MODE
|
|
73
|
+
[INTACT-STABILIZE | BROKEN-EXPAND | OVERDENSE-SPLIT]
|
|
74
|
+
|
|
75
|
+
GKP
|
|
76
|
+
- Patterns: [IDs consulted, spec files read]
|
|
77
|
+
- Constraints: [key constraints]
|
|
78
|
+
- Risks: [key risks]
|
|
79
|
+
|
|
80
|
+
ACTION PLAN (DECIDE+ACT)
|
|
81
|
+
- Core: [main logic change — files, functions]
|
|
82
|
+
- Shell: [boundary change — new params, exports, routes]
|
|
83
|
+
- Neighbor: [affected modules — who calls this, who breaks]
|
|
84
|
+
- Pattern: [which IDs apply]
|
|
85
|
+
- Risk: [what could go wrong — rollback strategy]
|
|
86
|
+
|
|
87
|
+
DISPOSITION
|
|
88
|
+
[IMPLEMENT NOW → hand to SyncLoop-Fixer | BACKLOG → store in docs/backlog/]
|
|
89
|
+
|
|
90
|
+
NEXT STEPS
|
|
91
|
+
[If implementing: instructions for SyncLoop-Fixer]
|
|
92
|
+
[If backlog: task file path + index update confirmation]
|
|
93
|
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
94
|
+
```
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: "SyncLoop-Fixer"
|
|
3
|
+
description: "SyncLoop subagent for implementation and validation. Runs CHALLENGE-TEST → UPDATE → LEARN. Use for executing Action Plans and fixing bugs."
|
|
4
|
+
argument-hint: "An Action Plan to implement or a bug to fix."
|
|
5
|
+
tools:
|
|
6
|
+
- "vscode"
|
|
7
|
+
- "execute"
|
|
8
|
+
- "read"
|
|
9
|
+
- "edit"
|
|
10
|
+
- "search"
|
|
11
|
+
- "todo"
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
You are the SyncLoop Fixer agent for this codebase.
|
|
15
|
+
|
|
16
|
+
Your role is to execute the second half of the **7-stage SyncLoop loop**:
|
|
17
|
+
`CHALLENGE-TEST → UPDATE → LEARN`
|
|
18
|
+
|
|
19
|
+
You take an **Action Plan** (from the user or the Architect agent), implement it, and run the validation gates.
|
|
20
|
+
|
|
21
|
+
---
|
|
22
|
+
|
|
23
|
+
## Spec Files to Load
|
|
24
|
+
|
|
25
|
+
| File | Purpose | Load At |
|
|
26
|
+
|------|---------|---------|
|
|
27
|
+
| `.agent-loop/reasoning-kernel.md` | Master loop, full stage detail | SENSE |
|
|
28
|
+
| `.agent-loop/validate-env.md` | Stage 1 gates: types, tests, layers, complexity | CHALLENGE-TEST |
|
|
29
|
+
| `.agent-loop/validate-n.md` | Stage 2 gates: shapes, boundaries, bridges | CHALLENGE-TEST |
|
|
30
|
+
| `.agent-loop/feedback.md` | Failure diagnosis, patch protocol, branch pruning | FEEDBACK |
|
|
31
|
+
|
|
32
|
+
---
|
|
33
|
+
|
|
34
|
+
## Output Schema
|
|
35
|
+
|
|
36
|
+
```
|
|
37
|
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
38
|
+
IMPLEMENTATION
|
|
39
|
+
[files changed, logic implemented]
|
|
40
|
+
|
|
41
|
+
CHALLENGE-TEST (iteration N/5)
|
|
42
|
+
[PASS | FAIL — reason]
|
|
43
|
+
|
|
44
|
+
UPDATE
|
|
45
|
+
[state transitions, commits]
|
|
46
|
+
|
|
47
|
+
LEARN
|
|
48
|
+
[what was persisted to patterns.md or patterns/*.md]
|
|
49
|
+
|
|
50
|
+
REPORT
|
|
51
|
+
[docs/reports/YYYY-MM-DD-{slug}.md — or "skipped (trivial)"]
|
|
52
|
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
53
|
+
```
|
|
@@ -120,7 +120,19 @@ Mode selected in DECIDE+ACT. Can change after each validation cycle.
|
|
|
120
120
|
4. **CHALLENGE-TEST** — Run `validate-env.md` then `validate-n.md`. Classify failures (see below). Loop until pass or budget exhausted.
|
|
121
121
|
5. **UPDATE** — Commit state transitions. If new issue found → one more CHALLENGE-TEST pass.
|
|
122
122
|
6. **LEARN** — Persist: quick fix → `patterns.md` table; deep pattern → `patterns/{spec}.md`; new term → `glossary.md`.
|
|
123
|
-
7. **REPORT** —
|
|
123
|
+
7. **REPORT** — Route output: implemented + multi-file → `docs/reports/`; planned but not implemented → `docs/backlog/`; trivial → skip.
|
|
124
|
+
|
|
125
|
+
### Report vs Backlog Routing
|
|
126
|
+
|
|
127
|
+
```
|
|
128
|
+
Work implemented this session?
|
|
129
|
+
├─ YES + multi-file or architecture change → write docs/reports/YYYY-MM-DD-{slug}.md
|
|
130
|
+
├─ YES + single-file cosmetic/docs-only → skip
|
|
131
|
+
├─ NO + investigation/plan produced → write docs/backlog/YYYY-MM-DD-{slug}.md + update index
|
|
132
|
+
└─ NO + trivial lookup/question → skip
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
Reports = completed work. Backlog tasks = planned but unexecuted work. Never create both for the same task.
|
|
124
136
|
|
|
125
137
|
### Failure Classification
|
|
126
138
|
|
|
@@ -194,7 +206,7 @@ LEARN
|
|
|
194
206
|
[what was persisted to patterns.md or patterns/*.md]
|
|
195
207
|
|
|
196
208
|
REPORT
|
|
197
|
-
[docs/reports/YYYY-MM-DD-{slug}.md — or "skipped (trivial)"]
|
|
209
|
+
[docs/reports/YYYY-MM-DD-{slug}.md | docs/backlog/YYYY-MM-DD-{slug}.md — or "skipped (trivial)"]
|
|
198
210
|
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
199
211
|
```
|
|
200
212
|
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: "diagnose-failure"
|
|
3
|
+
description: "Run the SyncLoop FEEDBACK loop to diagnose a test failure, type error, or layer violation. Use this when CHALLENGE-TEST fails."
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Diagnose Failure (SyncLoop FEEDBACK)
|
|
7
|
+
|
|
8
|
+
You are executing the **FEEDBACK** stage of the SyncLoop protocol.
|
|
9
|
+
|
|
10
|
+
## Instructions
|
|
11
|
+
|
|
12
|
+
1. Read `.agent-loop/feedback.md` to understand the patch protocol and branch pruning rules.
|
|
13
|
+
2. Analyze the failure (test output, type error, or layer violation).
|
|
14
|
+
3. Classify the failure as **Micro** or **Macro**.
|
|
15
|
+
4. Produce a patch.
|
|
16
|
+
5. If this is the 3rd time the same error has occurred, trigger **Branch Prune** and revert the approach.
|
|
17
|
+
|
|
18
|
+
## Output Schema
|
|
19
|
+
|
|
20
|
+
```
|
|
21
|
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
22
|
+
DIAGNOSIS
|
|
23
|
+
[Error analysis and classification: Micro vs Macro]
|
|
24
|
+
|
|
25
|
+
PATCH PLAN
|
|
26
|
+
[What needs to be changed to fix the error]
|
|
27
|
+
|
|
28
|
+
EXECUTION
|
|
29
|
+
[Apply the patch]
|
|
30
|
+
|
|
31
|
+
NEXT STEPS
|
|
32
|
+
[Instructions to re-run CHALLENGE-TEST]
|
|
33
|
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
34
|
+
```
|
package/package.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@oleksandr.rudnychenko/sync_loop",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.3",
|
|
4
4
|
"type": "module",
|
|
5
|
-
"description": "MCP server
|
|
5
|
+
"description": "Self-correcting 7-stage agent reasoning loop (SENSE→GKP→DECIDE+ACT→CHALLENGE-TEST→UPDATE→LEARN→REPORT). MCP server + CLI that scaffolds instruction files for GitHub Copilot, Cursor, and Claude Code.",
|
|
6
6
|
"bin": {
|
|
7
7
|
"sync_loop": "./bin/cli.js"
|
|
8
8
|
},
|
|
@@ -30,15 +30,27 @@
|
|
|
30
30
|
},
|
|
31
31
|
"keywords": [
|
|
32
32
|
"mcp",
|
|
33
|
+
"mcp-server",
|
|
34
|
+
"model-context-protocol",
|
|
33
35
|
"ai",
|
|
36
|
+
"ai-agent",
|
|
34
37
|
"agent",
|
|
38
|
+
"agentic",
|
|
35
39
|
"copilot",
|
|
40
|
+
"github-copilot",
|
|
36
41
|
"cursor",
|
|
37
42
|
"claude",
|
|
43
|
+
"claude-code",
|
|
44
|
+
"llm",
|
|
38
45
|
"prompt-engineering",
|
|
39
46
|
"coding-agent",
|
|
40
47
|
"reasoning-loop",
|
|
41
|
-
"
|
|
48
|
+
"self-correcting",
|
|
49
|
+
"scaffolding",
|
|
50
|
+
"instructions",
|
|
51
|
+
"sync_loop",
|
|
52
|
+
"syncloop",
|
|
53
|
+
"devtools"
|
|
42
54
|
],
|
|
43
55
|
"license": "MIT",
|
|
44
56
|
"author": "oleksandr.rudnychenko",
|
|
@@ -19,76 +19,27 @@ Referenced from [../patterns.md](../patterns.md).
|
|
|
19
19
|
|
|
20
20
|
## Workflow for New Routes
|
|
21
21
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
4. Run spec generation script
|
|
22
|
+
1. Define Request and Response **typed models** in the module's models file — every field has a declared type and sensible defaults for optional fields
|
|
23
|
+
2. Implement the route handler: parse input into the request model, resolve the service via dependency injection, call the service, and return the response model
|
|
24
|
+
3. Register the router in the app entrypoint with appropriate semantic tags
|
|
25
|
+
4. Run the spec generation script (OpenAPI, Swagger, or equivalent)
|
|
27
26
|
5. Verify generated docs match expectations
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
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
|
-
```
|
|
27
|
+
|
|
28
|
+
### Route Handler Structure
|
|
29
|
+
|
|
30
|
+
A route handler is a thin transport function. It declares the HTTP method and path, accepts a typed request model as input, resolves the service through the framework's dependency injection mechanism, delegates the business operation to the service, and returns a typed response model. The handler contains no business logic, conditionals, or data transformations beyond serialization.
|
|
67
31
|
|
|
68
32
|
---
|
|
69
33
|
|
|
70
34
|
## Error Envelope
|
|
71
35
|
|
|
72
|
-
All error responses must follow a consistent structure:
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
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
|
-
```
|
|
36
|
+
All error responses must follow a consistent structure with three fields:
|
|
37
|
+
|
|
38
|
+
- **error**: a machine-readable error code (string)
|
|
39
|
+
- **message**: a human-readable description (string)
|
|
40
|
+
- **details**: optional context dictionary for debugging
|
|
41
|
+
|
|
42
|
+
At the route boundary, catch domain-specific exceptions (NotFoundError, ValidationError) and translate them to the appropriate HTTP status code plus an error envelope. Let unexpected exceptions propagate to a global handler that returns a generic 500-level response.
|
|
92
43
|
|
|
93
44
|
---
|
|
94
45
|
|
|
@@ -9,34 +9,11 @@ Referenced from [../patterns.md](../patterns.md).
|
|
|
9
9
|
|
|
10
10
|
Abstracts infrastructure behind protocol interfaces. Decouples domain logic from external systems.
|
|
11
11
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
class
|
|
15
|
-
|
|
16
|
-
|
|
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
|
-
```
|
|
12
|
+
Define a **Port** as an interface (or protocol/trait) that declares the operations a service needs — method names, parameter types, and return types — without any implementation details. The Port belongs to the domain layer and knows nothing about databases, HTTP, or file systems.
|
|
13
|
+
|
|
14
|
+
Create an **Adapter** as a concrete class that implements the Port interface. Each adapter encapsulates the specifics of one external system (a database client, an HTTP API, a message queue). Multiple adapters can satisfy the same port.
|
|
15
|
+
|
|
16
|
+
Services receive the port interface via constructor injection, never the adapter directly.
|
|
40
17
|
|
|
41
18
|
**Key rules:**
|
|
42
19
|
- Port lives in `libs/{component}/port.*`
|
|
@@ -56,17 +33,11 @@ Each domain module follows a consistent multi-file layout:
|
|
|
56
33
|
| `routes.*` | Transport endpoints |
|
|
57
34
|
| `tasks.*` | Background tasks (if async processing needed) |
|
|
58
35
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
def __init__(self, repository: OrderRepository) -> None:
|
|
63
|
-
self._repository = repository
|
|
36
|
+
The **models** file defines data structures (classes, structs, or types) representing the core domain — entities with identity, value objects without identity, and any associated enums or constants.
|
|
37
|
+
|
|
38
|
+
The **services** file contains business logic: methods that operate on models, enforce invariants, and coordinate between ports. Services never reference transport or serialization concerns.
|
|
64
39
|
|
|
65
|
-
|
|
66
|
-
order = self._repository.get(order_id)
|
|
67
|
-
# business logic here
|
|
68
|
-
return ProcessResult(order_id=order.id, status="completed")
|
|
69
|
-
```
|
|
40
|
+
The **routes** file is the transport boundary — HTTP handlers, CLI entry points, or message consumers. Routes parse input, delegate to a service, and format the response. No business logic lives here.
|
|
70
41
|
|
|
71
42
|
---
|
|
72
43
|
|
|
@@ -74,219 +45,80 @@ class OrderService:
|
|
|
74
45
|
|
|
75
46
|
Task handlers stay thin. Business logic always lives in services.
|
|
76
47
|
|
|
77
|
-
|
|
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
|
-
```
|
|
48
|
+
A task handler is a function or method invoked by a job queue or scheduler. It receives a runtime context (containing pre-wired service instances) and the task payload. The handler calls the appropriate service method, updates task status on success and failure, and re-raises exceptions for the queue's retry mechanism.
|
|
91
49
|
|
|
92
50
|
**Key rules:**
|
|
93
51
|
- Tasks never contain business logic
|
|
94
|
-
- Dependencies injected via runtime, not imported directly
|
|
95
|
-
- Always update status on success and failure
|
|
52
|
+
- Dependencies injected via runtime context, not imported directly
|
|
53
|
+
- Always update status on both success and failure paths
|
|
96
54
|
|
|
97
55
|
---
|
|
98
56
|
|
|
99
57
|
## P4 · App Context / Composition Root
|
|
100
58
|
|
|
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
|
|
59
|
+
Centralized dependency wiring, initialized once at startup.
|
|
112
60
|
|
|
113
|
-
|
|
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
|
-
```
|
|
61
|
+
Define an **AppContext** as a container (class or struct) holding all application-wide dependencies: configuration, database session factories, service registries, and loggers. A factory function creates the context once during startup, caching it as a module-level singleton. All subsequent access goes through a getter that returns the cached instance. This ensures consistent wiring and makes the full dependency graph visible in one place.
|
|
125
62
|
|
|
126
63
|
---
|
|
127
64
|
|
|
128
65
|
## P5 · Transport Route
|
|
129
66
|
|
|
130
|
-
Routes only handle transport concerns; all logic delegated to services
|
|
131
|
-
|
|
132
|
-
```python
|
|
133
|
-
router = APIRouter()
|
|
67
|
+
Routes only handle transport concerns; all logic delegated to services.
|
|
134
68
|
|
|
135
|
-
|
|
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
|
-
```
|
|
69
|
+
A route handler performs three steps: (1) parse and validate the incoming request into a typed model, (2) resolve the appropriate service instance via dependency injection, and (3) call the service method and map the result to a typed response model. The handler never contains conditionals, loops, or data transformations beyond serialization.
|
|
147
70
|
|
|
148
71
|
---
|
|
149
72
|
|
|
150
73
|
## P6 · Typed Models
|
|
151
74
|
|
|
152
|
-
Domain entities with explicit types and serialization
|
|
153
|
-
|
|
154
|
-
|
|
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
|
-
```
|
|
75
|
+
Domain entities with explicit types and serialization.
|
|
76
|
+
|
|
77
|
+
Every data structure that crosses a boundary (API request, API response, database row, message payload) must be a typed model — a class, struct, or schema with named fields and declared types. Each model provides a serialization method (to dictionary, JSON, or equivalent) for transport. Use slot-based or frozen classes where the language supports them to prevent accidental mutation.
|
|
170
78
|
|
|
171
79
|
---
|
|
172
80
|
|
|
173
81
|
## P7 · Collection/Enum Safety
|
|
174
82
|
|
|
175
|
-
Replace magic strings with typed enums
|
|
83
|
+
Replace magic strings with typed enums.
|
|
176
84
|
|
|
177
|
-
|
|
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
|
-
```
|
|
85
|
+
Define an enum (or string enum / literal union) for any fixed set of values: collection names, status codes, category labels, entity types. All code references the enum member, never a raw string. This centralizes valid values in one declaration and makes invalid states unrepresentable.
|
|
186
86
|
|
|
187
87
|
---
|
|
188
88
|
|
|
189
89
|
## P8 · Error Handling
|
|
190
90
|
|
|
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."""
|
|
91
|
+
Layered exception hierarchy with boundary translation.
|
|
203
92
|
|
|
204
|
-
|
|
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
|
-
```
|
|
93
|
+
Define a base domain exception class. Derive specific exceptions from it: `NotFoundError`, `ValidationError`, `ConflictError`, etc. Business logic raises domain exceptions only. At the transport boundary (route handler), catch domain exceptions and translate them to the appropriate HTTP status codes or error responses. Internal implementation errors (unexpected crashes) propagate to a global handler that returns a generic 500-level response.
|
|
214
94
|
|
|
215
95
|
---
|
|
216
96
|
|
|
217
97
|
## P9 · Type Hints Everywhere
|
|
218
98
|
|
|
219
|
-
All code must have complete type annotations
|
|
99
|
+
All code must have complete type annotations.
|
|
220
100
|
|
|
221
|
-
|
|
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
|
-
```
|
|
101
|
+
Every function signature declares parameter types and return type. Use union types for nullable values. Use generic types for collections. Define type aliases for complex or repeated signatures (e.g., a factory callable, a filter mapping). Avoid `any` / untyped containers. The type checker must pass with zero errors on every commit.
|
|
240
102
|
|
|
241
103
|
---
|
|
242
104
|
|
|
243
105
|
## P10 · Service Orchestration
|
|
244
106
|
|
|
245
|
-
Services accept all dependencies via constructor — no hidden state
|
|
246
|
-
|
|
247
|
-
|
|
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
|
-
```
|
|
107
|
+
Services accept all dependencies via constructor — no hidden state.
|
|
108
|
+
|
|
109
|
+
A service class receives every collaborator (repositories, other services, external clients) through its constructor. The constructor stores them as private fields. No service creates its own dependencies internally. This makes the dependency graph explicit and enables test doubles to be injected without patching or monkey-patching.
|
|
264
110
|
|
|
265
111
|
---
|
|
266
112
|
|
|
267
113
|
## P11 · Config Isolation
|
|
268
114
|
|
|
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
|
|
115
|
+
Centralized, environment-based configuration with startup validation.
|
|
277
116
|
|
|
278
|
-
|
|
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
|
-
```
|
|
117
|
+
Define a configuration class with typed fields and default values. A class method reads all values from environment variables at startup, parsing them into the correct types. If a required variable is missing, startup fails immediately with a clear error. No other code reads environment variables directly — all access goes through the configuration instance.
|
|
286
118
|
|
|
287
119
|
**Key rules:**
|
|
288
120
|
- All config read from environment at startup
|
|
289
|
-
- No scattered
|
|
121
|
+
- No scattered environment variable calls inside business logic
|
|
290
122
|
- Test config overrides controlled via fixtures
|
|
291
123
|
|
|
292
124
|
---
|