devcoach 0.3.9__tar.gz → 0.3.10__tar.gz
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.
- {devcoach-0.3.9 → devcoach-0.3.10}/.gitignore +2 -0
- {devcoach-0.3.9 → devcoach-0.3.10}/PKG-INFO +30 -7
- {devcoach-0.3.9 → devcoach-0.3.10}/README.md +29 -6
- devcoach-0.3.10/docs/how-it-works.md +102 -0
- {devcoach-0.3.9 → devcoach-0.3.10}/docs/index.md +26 -21
- {devcoach-0.3.9 → devcoach-0.3.10}/pyproject.toml +1 -1
- {devcoach-0.3.9 → devcoach-0.3.10}/sonar-project.properties +1 -1
- {devcoach-0.3.9 → devcoach-0.3.10}/src/devcoach/mcp/server.py +42 -13
- {devcoach-0.3.9 → devcoach-0.3.10}/tests/test_mcp_server.py +151 -0
- {devcoach-0.3.9 → devcoach-0.3.10}/uv.lock +1 -1
- devcoach-0.3.9/docs/how-it-works.md +0 -112
- {devcoach-0.3.9 → devcoach-0.3.10}/.github/dependabot.yml +0 -0
- {devcoach-0.3.9 → devcoach-0.3.10}/.github/scripts/export_backup.py +0 -0
- {devcoach-0.3.9 → devcoach-0.3.10}/.github/scripts/fixtures/devcoach-backup.zip +0 -0
- {devcoach-0.3.9 → devcoach-0.3.10}/.github/scripts/take_screenshots.py +0 -0
- {devcoach-0.3.9 → devcoach-0.3.10}/.github/workflows/ci.yml +0 -0
- {devcoach-0.3.9 → devcoach-0.3.10}/.github/workflows/ruff-autofix.yml +0 -0
- {devcoach-0.3.9 → devcoach-0.3.10}/.github/workflows/update-screenshots.yml +0 -0
- {devcoach-0.3.9 → devcoach-0.3.10}/CLAUDE.md +0 -0
- {devcoach-0.3.9 → devcoach-0.3.10}/LICENSE +0 -0
- {devcoach-0.3.9 → devcoach-0.3.10}/NOTICE +0 -0
- {devcoach-0.3.9 → devcoach-0.3.10}/SKILL.md +0 -0
- {devcoach-0.3.9 → devcoach-0.3.10}/docs/PLAN.md +0 -0
- {devcoach-0.3.9 → devcoach-0.3.10}/docs/cli.md +0 -0
- {devcoach-0.3.9 → devcoach-0.3.10}/docs/configuration.md +0 -0
- {devcoach-0.3.9 → devcoach-0.3.10}/docs/favicon.svg +0 -0
- {devcoach-0.3.9 → devcoach-0.3.10}/docs/getting-started.md +0 -0
- {devcoach-0.3.9 → devcoach-0.3.10}/docs/mcp-server.md +0 -0
- {devcoach-0.3.9 → devcoach-0.3.10}/docs/screenshots/knowledge-map-dark.png +0 -0
- {devcoach-0.3.9 → devcoach-0.3.10}/docs/screenshots/knowledge-map-light.png +0 -0
- {devcoach-0.3.9 → devcoach-0.3.10}/docs/screenshots/lesson-ci-cd-pipeline-stages-dark.png +0 -0
- {devcoach-0.3.9 → devcoach-0.3.10}/docs/screenshots/lesson-ci-cd-pipeline-stages-light.png +0 -0
- {devcoach-0.3.9 → devcoach-0.3.10}/docs/screenshots/lesson-docker-layer-cache-dark.png +0 -0
- {devcoach-0.3.9 → devcoach-0.3.10}/docs/screenshots/lesson-docker-layer-cache-light.png +0 -0
- {devcoach-0.3.9 → devcoach-0.3.10}/docs/screenshots/lesson-git-interactive-rebase-dark.png +0 -0
- {devcoach-0.3.9 → devcoach-0.3.10}/docs/screenshots/lesson-git-interactive-rebase-light.png +0 -0
- {devcoach-0.3.9 → devcoach-0.3.10}/docs/screenshots/lesson-postgresql-explain-analyze-dark.png +0 -0
- {devcoach-0.3.9 → devcoach-0.3.10}/docs/screenshots/lesson-postgresql-explain-analyze-light.png +0 -0
- {devcoach-0.3.9 → devcoach-0.3.10}/docs/screenshots/lesson-redis-cache-stampede-dark.png +0 -0
- {devcoach-0.3.9 → devcoach-0.3.10}/docs/screenshots/lesson-redis-cache-stampede-light.png +0 -0
- {devcoach-0.3.9 → devcoach-0.3.10}/docs/screenshots/lessons-dark.png +0 -0
- {devcoach-0.3.9 → devcoach-0.3.10}/docs/screenshots/lessons-light.png +0 -0
- {devcoach-0.3.9 → devcoach-0.3.10}/docs/screenshots/settings-dark.png +0 -0
- {devcoach-0.3.9 → devcoach-0.3.10}/docs/screenshots/settings-light.png +0 -0
- {devcoach-0.3.9 → devcoach-0.3.10}/docs/web-ui.md +0 -0
- {devcoach-0.3.9 → devcoach-0.3.10}/mkdocs.yml +0 -0
- {devcoach-0.3.9 → devcoach-0.3.10}/src/devcoach/SKILL.md +0 -0
- {devcoach-0.3.9 → devcoach-0.3.10}/src/devcoach/__init__.py +0 -0
- {devcoach-0.3.9 → devcoach-0.3.10}/src/devcoach/cli/__init__.py +0 -0
- {devcoach-0.3.9 → devcoach-0.3.10}/src/devcoach/cli/commands.py +0 -0
- {devcoach-0.3.9 → devcoach-0.3.10}/src/devcoach/core/__init__.py +0 -0
- {devcoach-0.3.9 → devcoach-0.3.10}/src/devcoach/core/coach.py +0 -0
- {devcoach-0.3.9 → devcoach-0.3.10}/src/devcoach/core/db.py +0 -0
- {devcoach-0.3.9 → devcoach-0.3.10}/src/devcoach/core/detect.py +0 -0
- {devcoach-0.3.9 → devcoach-0.3.10}/src/devcoach/core/git.py +0 -0
- {devcoach-0.3.9 → devcoach-0.3.10}/src/devcoach/core/models.py +0 -0
- {devcoach-0.3.9 → devcoach-0.3.10}/src/devcoach/core/prompts.py +0 -0
- {devcoach-0.3.9 → devcoach-0.3.10}/src/devcoach/mcp/__init__.py +0 -0
- {devcoach-0.3.9 → devcoach-0.3.10}/src/devcoach/web/__init__.py +0 -0
- {devcoach-0.3.9 → devcoach-0.3.10}/src/devcoach/web/app.py +0 -0
- {devcoach-0.3.9 → devcoach-0.3.10}/src/devcoach/web/static/favicon.svg +0 -0
- {devcoach-0.3.9 → devcoach-0.3.10}/src/devcoach/web/static/relative-time.js +0 -0
- {devcoach-0.3.9 → devcoach-0.3.10}/src/devcoach/web/static/style.css +0 -0
- {devcoach-0.3.9 → devcoach-0.3.10}/src/devcoach/web/static/vendor/alpinejs.min.js +0 -0
- {devcoach-0.3.9 → devcoach-0.3.10}/src/devcoach/web/static/vendor/flatpickr-dark.min.css +0 -0
- {devcoach-0.3.9 → devcoach-0.3.10}/src/devcoach/web/static/vendor/flatpickr.min.css +0 -0
- {devcoach-0.3.9 → devcoach-0.3.10}/src/devcoach/web/static/vendor/flatpickr.min.js +0 -0
- {devcoach-0.3.9 → devcoach-0.3.10}/src/devcoach/web/static/vendor/highlight.min.js +0 -0
- {devcoach-0.3.9 → devcoach-0.3.10}/src/devcoach/web/static/vendor/hljs-dark.min.css +0 -0
- {devcoach-0.3.9 → devcoach-0.3.10}/src/devcoach/web/static/vendor/hljs-light.min.css +0 -0
- {devcoach-0.3.9 → devcoach-0.3.10}/src/devcoach/web/static/vendor/htmx.min.js +0 -0
- {devcoach-0.3.9 → devcoach-0.3.10}/src/devcoach/web/static/vendor/icons/bitbucket.svg +0 -0
- {devcoach-0.3.9 → devcoach-0.3.10}/src/devcoach/web/static/vendor/icons/github.svg +0 -0
- {devcoach-0.3.9 → devcoach-0.3.10}/src/devcoach/web/static/vendor/icons/gitlab.svg +0 -0
- {devcoach-0.3.9 → devcoach-0.3.10}/src/devcoach/web/static/vendor/icons/vscode.svg +0 -0
- {devcoach-0.3.9 → devcoach-0.3.10}/src/devcoach/web/static/vendor/marked.min.js +0 -0
- {devcoach-0.3.9 → devcoach-0.3.10}/src/devcoach/web/static/vendor/tailwind.js +0 -0
- {devcoach-0.3.9 → devcoach-0.3.10}/src/devcoach/web/templates/base.html +0 -0
- {devcoach-0.3.9 → devcoach-0.3.10}/src/devcoach/web/templates/lesson_detail.html +0 -0
- {devcoach-0.3.9 → devcoach-0.3.10}/src/devcoach/web/templates/lessons.html +0 -0
- {devcoach-0.3.9 → devcoach-0.3.10}/src/devcoach/web/templates/profile.html +0 -0
- {devcoach-0.3.9 → devcoach-0.3.10}/src/devcoach/web/templates/settings.html +0 -0
- {devcoach-0.3.9 → devcoach-0.3.10}/tests/__init__.py +0 -0
- {devcoach-0.3.9 → devcoach-0.3.10}/tests/conftest.py +0 -0
- {devcoach-0.3.9 → devcoach-0.3.10}/tests/test_cli.py +0 -0
- {devcoach-0.3.9 → devcoach-0.3.10}/tests/test_cli_commands.py +0 -0
- {devcoach-0.3.9 → devcoach-0.3.10}/tests/test_coach.py +0 -0
- {devcoach-0.3.9 → devcoach-0.3.10}/tests/test_db_extra.py +0 -0
- {devcoach-0.3.9 → devcoach-0.3.10}/tests/test_detect.py +0 -0
- {devcoach-0.3.9 → devcoach-0.3.10}/tests/test_git.py +0 -0
- {devcoach-0.3.9 → devcoach-0.3.10}/tests/test_prompts.py +0 -0
- {devcoach-0.3.9 → devcoach-0.3.10}/tests/test_web.py +0 -0
- {devcoach-0.3.9 → devcoach-0.3.10}/tests/test_web_extra.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: devcoach
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.10
|
|
4
4
|
Summary: A local MCP server that acts as a progressive technical coach for Claude Code and Claude Desktop
|
|
5
5
|
Project-URL: Homepage, https://github.com/UltimaPhoenix/dev-coach
|
|
6
6
|
Project-URL: Repository, https://github.com/UltimaPhoenix/dev-coach
|
|
@@ -245,12 +245,35 @@ Description-Content-Type: text/markdown
|
|
|
245
245
|
|
|
246
246
|
## How it works
|
|
247
247
|
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
248
|
+
```mermaid
|
|
249
|
+
flowchart TD
|
|
250
|
+
A([Task completed]) --> B[Check rate limit]
|
|
251
|
+
B -->|denied| Z([Silent])
|
|
252
|
+
B -->|allowed| D
|
|
253
|
+
|
|
254
|
+
subgraph loop["coaching loop"]
|
|
255
|
+
D[Select topic & depth]
|
|
256
|
+
E[Compose & deliver]
|
|
257
|
+
G[log_lesson]
|
|
258
|
+
end
|
|
259
|
+
|
|
260
|
+
D -->|nothing| Z
|
|
261
|
+
D -->|found| E
|
|
262
|
+
E --> G
|
|
263
|
+
G --> F([Done])
|
|
264
|
+
G -.->|prompts| U(["You: ✅ ❌ ⏭"])
|
|
265
|
+
|
|
266
|
+
style loop fill:none,stroke:#AAAAAA,stroke-dasharray:5 5,color:#757575
|
|
267
|
+
classDef action fill:#D4E4D8,stroke:#8BAF96,color:#1E1E1E
|
|
268
|
+
classDef term fill:#E8E8E4,stroke:#AAAAAA,color:#1E1E1E
|
|
269
|
+
classDef user fill:#F5EDE3,stroke:#D4A27F,color:#1E1E1E
|
|
270
|
+
|
|
271
|
+
class B,D,E,G action
|
|
272
|
+
class A,F,Z term
|
|
273
|
+
class U user
|
|
274
|
+
```
|
|
275
|
+
|
|
276
|
+
→ [Full decision flow: session startup · lesson selection · depth calibration](https://ultimaphoenix.github.io/dev-coach/how-it-works/)
|
|
254
277
|
|
|
255
278
|
Everything runs **locally**. No data leaves your machine. One SQLite file at `~/.devcoach/coaching.db`.
|
|
256
279
|
|
|
@@ -14,12 +14,35 @@
|
|
|
14
14
|
|
|
15
15
|
## How it works
|
|
16
16
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
17
|
+
```mermaid
|
|
18
|
+
flowchart TD
|
|
19
|
+
A([Task completed]) --> B[Check rate limit]
|
|
20
|
+
B -->|denied| Z([Silent])
|
|
21
|
+
B -->|allowed| D
|
|
22
|
+
|
|
23
|
+
subgraph loop["coaching loop"]
|
|
24
|
+
D[Select topic & depth]
|
|
25
|
+
E[Compose & deliver]
|
|
26
|
+
G[log_lesson]
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
D -->|nothing| Z
|
|
30
|
+
D -->|found| E
|
|
31
|
+
E --> G
|
|
32
|
+
G --> F([Done])
|
|
33
|
+
G -.->|prompts| U(["You: ✅ ❌ ⏭"])
|
|
34
|
+
|
|
35
|
+
style loop fill:none,stroke:#AAAAAA,stroke-dasharray:5 5,color:#757575
|
|
36
|
+
classDef action fill:#D4E4D8,stroke:#8BAF96,color:#1E1E1E
|
|
37
|
+
classDef term fill:#E8E8E4,stroke:#AAAAAA,color:#1E1E1E
|
|
38
|
+
classDef user fill:#F5EDE3,stroke:#D4A27F,color:#1E1E1E
|
|
39
|
+
|
|
40
|
+
class B,D,E,G action
|
|
41
|
+
class A,F,Z term
|
|
42
|
+
class U user
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
→ [Full decision flow: session startup · lesson selection · depth calibration](https://ultimaphoenix.github.io/dev-coach/how-it-works/)
|
|
23
46
|
|
|
24
47
|
Everything runs **locally**. No data leaves your machine. One SQLite file at `~/.devcoach/coaching.db`.
|
|
25
48
|
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
# How it works
|
|
2
|
+
|
|
3
|
+
devcoach is a silent technical coach that hooks into every Claude response.
|
|
4
|
+
The diagrams below show the three main flows: session startup, the coaching loop,
|
|
5
|
+
and how a lesson topic is selected.
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## Session startup
|
|
10
|
+
|
|
11
|
+
At the start of each Claude session devcoach checks whether the user is set up,
|
|
12
|
+
loads prior coaching context, and primes lesson selection before any task is done.
|
|
13
|
+
|
|
14
|
+
```mermaid
|
|
15
|
+
flowchart LR
|
|
16
|
+
A([Start]) --> B{First run?}
|
|
17
|
+
B -- yes --> C[Detect stack]
|
|
18
|
+
C --> D[Confirm topics\n& groups]
|
|
19
|
+
D --> E[Save profile]
|
|
20
|
+
B -- no --> F[Load profile\n& notebook]
|
|
21
|
+
E & F --> G([Ready])
|
|
22
|
+
|
|
23
|
+
subgraph onboarding["onboarding"]
|
|
24
|
+
C
|
|
25
|
+
D
|
|
26
|
+
E
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
style onboarding fill:none,stroke:#AAAAAA,stroke-dasharray:5 5,color:#757575
|
|
30
|
+
classDef action fill:#D4E4D8,stroke:#8BAF96,color:#1E1E1E
|
|
31
|
+
classDef term fill:#E8E8E4,stroke:#AAAAAA,color:#1E1E1E
|
|
32
|
+
|
|
33
|
+
class C,D,E,F action
|
|
34
|
+
class A,G term
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
---
|
|
38
|
+
|
|
39
|
+
## Coaching loop
|
|
40
|
+
|
|
41
|
+
After every technical task Claude evaluates whether to deliver a lesson.
|
|
42
|
+
The loop is silent when nothing is worth teaching or when the rate limit is reached.
|
|
43
|
+
|
|
44
|
+
```mermaid
|
|
45
|
+
flowchart TD
|
|
46
|
+
A([Task completed]) --> B[Check rate limit]
|
|
47
|
+
B -->|denied| Z([Silent])
|
|
48
|
+
B -->|allowed| D
|
|
49
|
+
|
|
50
|
+
subgraph loop["coaching loop"]
|
|
51
|
+
D[Select topic & depth]
|
|
52
|
+
E[Compose & deliver]
|
|
53
|
+
G[log_lesson]
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
D -->|nothing| Z
|
|
57
|
+
D -->|found| E
|
|
58
|
+
E --> G
|
|
59
|
+
G --> F([Done])
|
|
60
|
+
G -.->|prompts| U(["You: ✅ ❌ ⏭"])
|
|
61
|
+
|
|
62
|
+
style loop fill:none,stroke:#AAAAAA,stroke-dasharray:5 5,color:#757575
|
|
63
|
+
classDef action fill:#D4E4D8,stroke:#8BAF96,color:#1E1E1E
|
|
64
|
+
classDef term fill:#E8E8E4,stroke:#AAAAAA,color:#1E1E1E
|
|
65
|
+
classDef user fill:#F5EDE3,stroke:#D4A27F,color:#1E1E1E
|
|
66
|
+
|
|
67
|
+
class B,D,E,G action
|
|
68
|
+
class A,F,Z term
|
|
69
|
+
class U user
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
---
|
|
73
|
+
|
|
74
|
+
## Lesson selection
|
|
75
|
+
|
|
76
|
+
When a teachable concept is found, devcoach walks this priority list from top to bottom
|
|
77
|
+
and picks the first match. Depth is then calibrated to the per-topic confidence score.
|
|
78
|
+
|
|
79
|
+
| Priority | Trigger | Condition |
|
|
80
|
+
|:---:|---|---|
|
|
81
|
+
| ① | Notebook follow-up | The coaching notebook flagged an angle relevant to the current task |
|
|
82
|
+
| ② | Profile pitfall | A pitfall committed or avoided on a profile topic |
|
|
83
|
+
| ③ | Profile pattern | An interesting pattern on a profile topic worth formalising |
|
|
84
|
+
| ④ | Off-profile pitfall | A pitfall on a topic prominent in the task but absent from the profile |
|
|
85
|
+
| ⑤ | Knowledge gap | A profile topic with confidence < 5 |
|
|
86
|
+
| ⑥ | Deep-dive | A profile topic at confidence 4–6, not yet mastered |
|
|
87
|
+
|
|
88
|
+
First match wins. No match → silent.
|
|
89
|
+
|
|
90
|
+
---
|
|
91
|
+
|
|
92
|
+
## Depth calibration
|
|
93
|
+
|
|
94
|
+
The lesson level is determined by the confidence score for the **specific topic being taught**,
|
|
95
|
+
adjusted by observations in the coaching notebook.
|
|
96
|
+
|
|
97
|
+
| Confidence | Level | Lesson angle |
|
|
98
|
+
|---|---|---|
|
|
99
|
+
| 0 – 3 | Junior | Introduce correct practice, explain from scratch, use analogies |
|
|
100
|
+
| 4 – 6 | Mid | Explain the why, mention trade-offs and alternatives |
|
|
101
|
+
| 7 – 9 | Senior | Edge cases, historical context, architectural implications |
|
|
102
|
+
| 10 | Cutting-edge | Latest developments — ignores level floor and taught-topics filter |
|
|
@@ -9,29 +9,34 @@ Everything runs **locally**. No data leaves your machine. One SQLite file at `~/
|
|
|
9
9
|
## How it works
|
|
10
10
|
|
|
11
11
|
```mermaid
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
12
|
+
flowchart TD
|
|
13
|
+
A([Task completed]) --> B[Check rate limit]
|
|
14
|
+
B -->|denied| Z([Silent])
|
|
15
|
+
B -->|allowed| D
|
|
16
|
+
|
|
17
|
+
subgraph loop["coaching loop"]
|
|
18
|
+
D[Select topic & depth]
|
|
19
|
+
E[Compose & deliver]
|
|
20
|
+
G[log_lesson]
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
D -->|nothing| Z
|
|
24
|
+
D -->|found| E
|
|
25
|
+
E --> G
|
|
26
|
+
G --> F([Done])
|
|
27
|
+
G -.->|prompts| U(["You: ✅ ❌ ⏭"])
|
|
28
|
+
|
|
29
|
+
style loop fill:none,stroke:#AAAAAA,stroke-dasharray:5 5,color:#757575
|
|
30
|
+
classDef action fill:#D4E4D8,stroke:#8BAF96,color:#1E1E1E
|
|
31
|
+
classDef term fill:#E8E8E4,stroke:#AAAAAA,color:#1E1E1E
|
|
32
|
+
classDef user fill:#F5EDE3,stroke:#D4A27F,color:#1E1E1E
|
|
33
|
+
|
|
34
|
+
class B,D,E,G action
|
|
35
|
+
class A,F,Z term
|
|
36
|
+
class U user
|
|
32
37
|
```
|
|
33
38
|
|
|
34
|
-
|
|
39
|
+
→ [Full decision flow: session startup · lesson selection · depth calibration](how-it-works.md)
|
|
35
40
|
|
|
36
41
|
---
|
|
37
42
|
|
|
@@ -9,6 +9,7 @@ from pathlib import Path
|
|
|
9
9
|
from typing import Literal
|
|
10
10
|
|
|
11
11
|
from fastmcp import Context, FastMCP
|
|
12
|
+
from mcp.types import ToolAnnotations
|
|
12
13
|
|
|
13
14
|
from devcoach.core import coach, db
|
|
14
15
|
from devcoach.core.detect import detect_stack
|
|
@@ -29,7 +30,9 @@ mcp = FastMCP(
|
|
|
29
30
|
# ── MCP Tools ─────────────────────────────────────────────────────────────
|
|
30
31
|
|
|
31
32
|
|
|
32
|
-
@mcp.tool
|
|
33
|
+
@mcp.tool(
|
|
34
|
+
annotations=ToolAnnotations(destructiveHint=False, idempotentHint=True, openWorldHint=False)
|
|
35
|
+
)
|
|
33
36
|
async def log_lesson(
|
|
34
37
|
ctx: Context,
|
|
35
38
|
id: str,
|
|
@@ -107,7 +110,9 @@ async def log_lesson(
|
|
|
107
110
|
return lesson
|
|
108
111
|
|
|
109
112
|
|
|
110
|
-
@mcp.tool
|
|
113
|
+
@mcp.tool(
|
|
114
|
+
annotations=ToolAnnotations(destructiveHint=False, idempotentHint=True, openWorldHint=False)
|
|
115
|
+
)
|
|
111
116
|
async def update_knowledge(ctx: Context, topic: str, delta: int) -> int:
|
|
112
117
|
"""Adjust the confidence score for a topic by delta (e.g. +1 or -1).
|
|
113
118
|
|
|
@@ -122,7 +127,11 @@ async def update_knowledge(ctx: Context, topic: str, delta: int) -> int:
|
|
|
122
127
|
raise
|
|
123
128
|
|
|
124
129
|
|
|
125
|
-
@mcp.tool
|
|
130
|
+
@mcp.tool(
|
|
131
|
+
annotations=ToolAnnotations(
|
|
132
|
+
readOnlyHint=True, destructiveHint=False, idempotentHint=True, openWorldHint=False
|
|
133
|
+
)
|
|
134
|
+
)
|
|
126
135
|
def get_lessons(
|
|
127
136
|
period: Literal["today", "week", "month", "year", "all"] | None = None,
|
|
128
137
|
category: str | None = None,
|
|
@@ -173,7 +182,9 @@ def get_lessons(
|
|
|
173
182
|
return []
|
|
174
183
|
|
|
175
184
|
|
|
176
|
-
@mcp.tool
|
|
185
|
+
@mcp.tool(
|
|
186
|
+
annotations=ToolAnnotations(destructiveHint=False, idempotentHint=True, openWorldHint=False)
|
|
187
|
+
)
|
|
177
188
|
async def star_lesson(ctx: Context, lesson_id: str, starred: bool) -> bool:
|
|
178
189
|
"""Set the starred (favourite) flag on a lesson to the given value.
|
|
179
190
|
|
|
@@ -192,7 +203,9 @@ async def star_lesson(ctx: Context, lesson_id: str, starred: bool) -> bool:
|
|
|
192
203
|
return False
|
|
193
204
|
|
|
194
205
|
|
|
195
|
-
@mcp.tool
|
|
206
|
+
@mcp.tool(
|
|
207
|
+
annotations=ToolAnnotations(destructiveHint=True, idempotentHint=True, openWorldHint=False)
|
|
208
|
+
)
|
|
196
209
|
async def delete_lesson(ctx: Context, lesson_id: str) -> bool:
|
|
197
210
|
"""Permanently delete a lesson by ID.
|
|
198
211
|
|
|
@@ -209,7 +222,9 @@ async def delete_lesson(ctx: Context, lesson_id: str) -> bool:
|
|
|
209
222
|
return False
|
|
210
223
|
|
|
211
224
|
|
|
212
|
-
@mcp.tool
|
|
225
|
+
@mcp.tool(
|
|
226
|
+
annotations=ToolAnnotations(destructiveHint=False, idempotentHint=True, openWorldHint=False)
|
|
227
|
+
)
|
|
213
228
|
async def submit_feedback(
|
|
214
229
|
ctx: Context, lesson_id: str, feedback: Literal["know", "dont_know", "clear"]
|
|
215
230
|
) -> bool:
|
|
@@ -244,7 +259,9 @@ async def submit_feedback(
|
|
|
244
259
|
return False
|
|
245
260
|
|
|
246
261
|
|
|
247
|
-
@mcp.tool
|
|
262
|
+
@mcp.tool(
|
|
263
|
+
annotations=ToolAnnotations(destructiveHint=False, idempotentHint=True, openWorldHint=False)
|
|
264
|
+
)
|
|
248
265
|
async def add_topic(
|
|
249
266
|
ctx: Context, topic: str, confidence: int = 5, group: str | None = None
|
|
250
267
|
) -> bool:
|
|
@@ -267,7 +284,9 @@ async def add_topic(
|
|
|
267
284
|
return False
|
|
268
285
|
|
|
269
286
|
|
|
270
|
-
@mcp.tool
|
|
287
|
+
@mcp.tool(
|
|
288
|
+
annotations=ToolAnnotations(destructiveHint=True, idempotentHint=True, openWorldHint=False)
|
|
289
|
+
)
|
|
271
290
|
async def remove_topic(ctx: Context, topic: str) -> bool:
|
|
272
291
|
"""Remove a topic from the knowledge map entirely.
|
|
273
292
|
|
|
@@ -284,7 +303,9 @@ async def remove_topic(ctx: Context, topic: str) -> bool:
|
|
|
284
303
|
return False
|
|
285
304
|
|
|
286
305
|
|
|
287
|
-
@mcp.tool
|
|
306
|
+
@mcp.tool(
|
|
307
|
+
annotations=ToolAnnotations(destructiveHint=False, idempotentHint=True, openWorldHint=False)
|
|
308
|
+
)
|
|
288
309
|
async def add_group(ctx: Context, name: str) -> bool:
|
|
289
310
|
"""Create a new (initially empty) knowledge group.
|
|
290
311
|
|
|
@@ -305,7 +326,9 @@ async def add_group(ctx: Context, name: str) -> bool:
|
|
|
305
326
|
return False
|
|
306
327
|
|
|
307
328
|
|
|
308
|
-
@mcp.tool
|
|
329
|
+
@mcp.tool(
|
|
330
|
+
annotations=ToolAnnotations(destructiveHint=True, idempotentHint=True, openWorldHint=False)
|
|
331
|
+
)
|
|
309
332
|
async def remove_group(ctx: Context, name: str) -> bool:
|
|
310
333
|
"""Delete a knowledge group. Topics in the group move to Other.
|
|
311
334
|
|
|
@@ -322,7 +345,9 @@ async def remove_group(ctx: Context, name: str) -> bool:
|
|
|
322
345
|
return False
|
|
323
346
|
|
|
324
347
|
|
|
325
|
-
@mcp.tool
|
|
348
|
+
@mcp.tool(
|
|
349
|
+
annotations=ToolAnnotations(destructiveHint=False, idempotentHint=True, openWorldHint=False)
|
|
350
|
+
)
|
|
326
351
|
def update_settings(key: Literal["max_per_day", "min_gap_minutes"], value: str) -> Settings:
|
|
327
352
|
"""Update a coaching setting.
|
|
328
353
|
|
|
@@ -345,7 +370,9 @@ def update_settings(key: Literal["max_per_day", "min_gap_minutes"], value: str)
|
|
|
345
370
|
return db.get_settings(conn)
|
|
346
371
|
|
|
347
372
|
|
|
348
|
-
@mcp.tool
|
|
373
|
+
@mcp.tool(
|
|
374
|
+
annotations=ToolAnnotations(destructiveHint=False, idempotentHint=False, openWorldHint=True)
|
|
375
|
+
)
|
|
349
376
|
def open_ui(port: int = 7860) -> str:
|
|
350
377
|
"""Launch the devcoach web dashboard in the background.
|
|
351
378
|
|
|
@@ -363,7 +390,9 @@ def open_ui(port: int = 7860) -> str:
|
|
|
363
390
|
return f"devcoach UI starting at http://localhost:{port}"
|
|
364
391
|
|
|
365
392
|
|
|
366
|
-
@mcp.tool
|
|
393
|
+
@mcp.tool(
|
|
394
|
+
annotations=ToolAnnotations(destructiveHint=True, idempotentHint=False, openWorldHint=False)
|
|
395
|
+
)
|
|
367
396
|
async def complete_onboarding(
|
|
368
397
|
ctx: Context,
|
|
369
398
|
topics: dict[str, int],
|
|
@@ -479,6 +479,12 @@ class TestCompleteOnboarding:
|
|
|
479
479
|
_run(server.complete_onboarding(mock_ctx, topics={"python": 7}))
|
|
480
480
|
mock_ctx.info.assert_called_once()
|
|
481
481
|
|
|
482
|
+
def test_exception_returns_empty_profile(self, mock_ctx):
|
|
483
|
+
with patch.object(db, "connection", side_effect=sqlite3.OperationalError("err")):
|
|
484
|
+
result = _run(server.complete_onboarding(mock_ctx, topics={"python": 7}))
|
|
485
|
+
assert result.knowledge == []
|
|
486
|
+
assert result.groups == []
|
|
487
|
+
|
|
482
488
|
|
|
483
489
|
# ── Resources ──────────────────────────────────────────────────────────────
|
|
484
490
|
|
|
@@ -557,6 +563,151 @@ class TestPrompt:
|
|
|
557
563
|
assert any(word in result.lower() for word in ("lesson", "coach", "devcoach", "knowledge"))
|
|
558
564
|
|
|
559
565
|
|
|
566
|
+
# ── Tools — delete_lesson ─────────────────────────────────────────────────
|
|
567
|
+
|
|
568
|
+
|
|
569
|
+
class TestDeleteLesson:
|
|
570
|
+
def test_found_returns_true(self, mock_ctx):
|
|
571
|
+
result = _run(server.delete_lesson(mock_ctx, "lesson-sqlite3-row-factory-001"))
|
|
572
|
+
assert result is True
|
|
573
|
+
|
|
574
|
+
def test_not_found_returns_false_and_logs_warning(self, mock_ctx):
|
|
575
|
+
result = _run(server.delete_lesson(mock_ctx, "nonexistent-id"))
|
|
576
|
+
assert result is False
|
|
577
|
+
mock_ctx.warning.assert_called_once()
|
|
578
|
+
|
|
579
|
+
def test_exception_returns_false_and_logs_error(self, mock_ctx):
|
|
580
|
+
with patch.object(db, "connection", side_effect=sqlite3.OperationalError("err")):
|
|
581
|
+
result = _run(server.delete_lesson(mock_ctx, "any-id"))
|
|
582
|
+
assert result is False
|
|
583
|
+
mock_ctx.error.assert_called_once()
|
|
584
|
+
|
|
585
|
+
|
|
586
|
+
# ── Exception paths ────────────────────────────────────────────────────────
|
|
587
|
+
|
|
588
|
+
|
|
589
|
+
class TestExceptionPaths:
|
|
590
|
+
"""Cover all exception-handler branches that return safe fallbacks."""
|
|
591
|
+
|
|
592
|
+
def test_log_lesson_usage_defaults_exception_falls_back(self, mock_ctx):
|
|
593
|
+
with patch(
|
|
594
|
+
"devcoach.core.db.get_usage_defaults", side_effect=sqlite3.OperationalError("err")
|
|
595
|
+
):
|
|
596
|
+
result = _run(
|
|
597
|
+
server.log_lesson(
|
|
598
|
+
mock_ctx,
|
|
599
|
+
id="exc-log-001",
|
|
600
|
+
timestamp=datetime.now(UTC).isoformat(),
|
|
601
|
+
topic_id="exc_topic",
|
|
602
|
+
categories=["test"],
|
|
603
|
+
title="Exc test",
|
|
604
|
+
level="mid",
|
|
605
|
+
summary="exc summary",
|
|
606
|
+
)
|
|
607
|
+
)
|
|
608
|
+
assert result.id == "exc-log-001"
|
|
609
|
+
|
|
610
|
+
def test_update_knowledge_exception_raises_and_logs_error(self, mock_ctx):
|
|
611
|
+
with patch.object(db, "connection", side_effect=sqlite3.OperationalError("err")):
|
|
612
|
+
with pytest.raises(sqlite3.OperationalError):
|
|
613
|
+
_run(server.update_knowledge(mock_ctx, "python", 1))
|
|
614
|
+
mock_ctx.error.assert_called_once()
|
|
615
|
+
|
|
616
|
+
def test_get_lessons_exception_returns_empty_list(self):
|
|
617
|
+
with patch.object(db, "connection", side_effect=sqlite3.OperationalError("err")):
|
|
618
|
+
result = server.get_lessons()
|
|
619
|
+
assert result == []
|
|
620
|
+
|
|
621
|
+
def test_star_lesson_exception_returns_false(self, mock_ctx):
|
|
622
|
+
with patch.object(db, "connection", side_effect=sqlite3.OperationalError("err")):
|
|
623
|
+
result = _run(server.star_lesson(mock_ctx, "any-id", starred=True))
|
|
624
|
+
assert result is False
|
|
625
|
+
mock_ctx.error.assert_called_once()
|
|
626
|
+
|
|
627
|
+
def test_submit_feedback_exception_returns_false(self, mock_ctx):
|
|
628
|
+
with patch.object(db, "connection", side_effect=sqlite3.OperationalError("err")):
|
|
629
|
+
result = _run(server.submit_feedback(mock_ctx, "any-id", "know"))
|
|
630
|
+
assert result is False
|
|
631
|
+
mock_ctx.error.assert_called_once()
|
|
632
|
+
|
|
633
|
+
def test_add_topic_exception_returns_false(self, mock_ctx):
|
|
634
|
+
with patch.object(db, "connection", side_effect=sqlite3.OperationalError("err")):
|
|
635
|
+
result = _run(server.add_topic(mock_ctx, "new_topic", 5))
|
|
636
|
+
assert result is False
|
|
637
|
+
mock_ctx.error.assert_called_once()
|
|
638
|
+
|
|
639
|
+
def test_remove_topic_exception_returns_false(self, mock_ctx):
|
|
640
|
+
with patch.object(db, "connection", side_effect=sqlite3.OperationalError("err")):
|
|
641
|
+
result = _run(server.remove_topic(mock_ctx, "python"))
|
|
642
|
+
assert result is False
|
|
643
|
+
mock_ctx.error.assert_called_once()
|
|
644
|
+
|
|
645
|
+
def test_add_group_exception_returns_false(self, mock_ctx):
|
|
646
|
+
with patch.object(db, "connection", side_effect=sqlite3.OperationalError("err")):
|
|
647
|
+
result = _run(server.add_group(mock_ctx, "SomeGroup"))
|
|
648
|
+
assert result is False
|
|
649
|
+
mock_ctx.error.assert_called_once()
|
|
650
|
+
|
|
651
|
+
def test_remove_group_exception_returns_false(self, mock_ctx):
|
|
652
|
+
with patch.object(db, "connection", side_effect=sqlite3.OperationalError("err")):
|
|
653
|
+
result = _run(server.remove_group(mock_ctx, "SomeGroup"))
|
|
654
|
+
assert result is False
|
|
655
|
+
mock_ctx.error.assert_called_once()
|
|
656
|
+
|
|
657
|
+
def test_profile_resource_exception_returns_error_dict(self):
|
|
658
|
+
with patch.object(db, "connection", side_effect=sqlite3.OperationalError("err")):
|
|
659
|
+
result = server.profile_resource()
|
|
660
|
+
assert "error" in result
|
|
661
|
+
|
|
662
|
+
def test_settings_resource_exception_returns_error_dict(self):
|
|
663
|
+
with patch.object(db, "connection", side_effect=sqlite3.OperationalError("err")):
|
|
664
|
+
result = server.settings_resource()
|
|
665
|
+
assert "error" in result
|
|
666
|
+
|
|
667
|
+
def test_recent_lessons_resource_exception_returns_error_list(self):
|
|
668
|
+
with patch.object(db, "connection", side_effect=sqlite3.OperationalError("err")):
|
|
669
|
+
result = server.recent_lessons_resource()
|
|
670
|
+
assert isinstance(result, list)
|
|
671
|
+
assert "error" in result[0]
|
|
672
|
+
|
|
673
|
+
def test_stats_resource_exception_returns_error_dict(self):
|
|
674
|
+
with patch.object(db, "connection", side_effect=sqlite3.OperationalError("err")):
|
|
675
|
+
result = server.stats_resource()
|
|
676
|
+
assert "error" in result
|
|
677
|
+
|
|
678
|
+
def test_taught_topics_resource_exception_returns_empty_list(self):
|
|
679
|
+
with patch.object(db, "connection", side_effect=sqlite3.OperationalError("err")):
|
|
680
|
+
result = server.taught_topics_resource()
|
|
681
|
+
assert result == []
|
|
682
|
+
|
|
683
|
+
def test_rate_limit_resource_exception_returns_not_allowed(self):
|
|
684
|
+
with patch.object(db, "connection", side_effect=sqlite3.OperationalError("err")):
|
|
685
|
+
result = server.rate_limit_resource()
|
|
686
|
+
assert result["allowed"] is False
|
|
687
|
+
|
|
688
|
+
def test_context_resource_exception_returns_error_dict(self):
|
|
689
|
+
with patch.object(db, "connection", side_effect=sqlite3.OperationalError("err")):
|
|
690
|
+
result = server.context_resource()
|
|
691
|
+
assert "error" in result
|
|
692
|
+
|
|
693
|
+
def test_onboarding_resource_exception_returns_error_dict(self):
|
|
694
|
+
with patch.object(db, "connection", side_effect=sqlite3.OperationalError("err")):
|
|
695
|
+
result = server.onboarding_resource()
|
|
696
|
+
assert "error" in result
|
|
697
|
+
|
|
698
|
+
def test_lesson_resource_exception_returns_error_dict(self):
|
|
699
|
+
with patch.object(db, "connection", side_effect=sqlite3.OperationalError("err")):
|
|
700
|
+
result = server.lesson_resource("any-id")
|
|
701
|
+
assert "error" in result
|
|
702
|
+
|
|
703
|
+
def test_devcoach_instructions_exception_returns_fallback(self):
|
|
704
|
+
with patch(
|
|
705
|
+
"devcoach.mcp.server.importlib.resources.files", side_effect=Exception("not found")
|
|
706
|
+
):
|
|
707
|
+
result = server.devcoach_instructions()
|
|
708
|
+
assert "unavailable" in result
|
|
709
|
+
|
|
710
|
+
|
|
560
711
|
# ── Entry point ────────────────────────────────────────────────────────────
|
|
561
712
|
|
|
562
713
|
|
|
@@ -1,112 +0,0 @@
|
|
|
1
|
-
# How it works
|
|
2
|
-
|
|
3
|
-
devcoach is a silent technical coach that hooks into every Claude response.
|
|
4
|
-
The diagrams below show the three main flows: session startup, the coaching loop,
|
|
5
|
-
and how a lesson topic is selected.
|
|
6
|
-
|
|
7
|
-
---
|
|
8
|
-
|
|
9
|
-
## Session startup
|
|
10
|
-
|
|
11
|
-
At the start of each Claude session devcoach checks whether the user is set up,
|
|
12
|
-
loads prior coaching context, and primes lesson selection before any task is done.
|
|
13
|
-
|
|
14
|
-
```mermaid
|
|
15
|
-
flowchart TD
|
|
16
|
-
A([Session starts]) --> B[Read devcoach://onboarding]
|
|
17
|
-
B --> C{needs_onboarding?}
|
|
18
|
-
C -- Yes --> D{Existing backup\nto restore?}
|
|
19
|
-
D -- Yes --> E[Restore backup\n→ mark onboarding done]
|
|
20
|
-
D -- No --> F[Detect stack\nautomatically or manually]
|
|
21
|
-
F --> G[Confirm topics + confidence\nPropose groups]
|
|
22
|
-
G --> H[complete_onboarding]
|
|
23
|
-
E & H --> I
|
|
24
|
-
C -- No --> I[Read ~/.devcoach/learning-state.md]
|
|
25
|
-
I --> J{Notebook\nnon-empty?}
|
|
26
|
-
J -- Yes --> K[Load patterns, hypotheses\nand recommended angles]
|
|
27
|
-
J -- No --> L[No prior context\nstart fresh]
|
|
28
|
-
K & L --> M([Ready to coach])
|
|
29
|
-
```
|
|
30
|
-
|
|
31
|
-
---
|
|
32
|
-
|
|
33
|
-
## Coaching loop
|
|
34
|
-
|
|
35
|
-
After every technical task Claude evaluates whether to deliver a lesson.
|
|
36
|
-
The loop is silent when nothing is worth teaching or when the rate limit is reached.
|
|
37
|
-
|
|
38
|
-
```mermaid
|
|
39
|
-
flowchart TD
|
|
40
|
-
A([Technical task\ncompleted]) --> B[Read devcoach://rate-limit]
|
|
41
|
-
B --> C{Allowed?}
|
|
42
|
-
C -- No\nnormal task --> Z([Silent —\nno lesson])
|
|
43
|
-
C -- No\nexplicit request --> D
|
|
44
|
-
C -- Yes --> D[Read profile\ntaught topics\ncoaching notebook]
|
|
45
|
-
D --> E[Analyse task for\nteachable concepts]
|
|
46
|
-
E --> F[Select topic\nsee Lesson selection]
|
|
47
|
-
F --> G{Topic\nfound?}
|
|
48
|
-
G -- No --> Z
|
|
49
|
-
G -- Yes --> H[Compose lesson\ncalibrate depth per-topic]
|
|
50
|
-
H --> I[log_lesson to MCP]
|
|
51
|
-
I --> J([Lesson appears\nat end of response])
|
|
52
|
-
J --> K{User\nfeedback}
|
|
53
|
-
K -- ✅ know --> L{Confidence\nbelow level band?}
|
|
54
|
-
L -- Yes --> M[submit_feedback\nconfidence +1]
|
|
55
|
-
L -- No --> N[Skip — already calibrated]
|
|
56
|
-
K -- ❌ don't know --> O[submit_feedback\nconfidence −1]
|
|
57
|
-
K -- ⏭ skip --> P[No change]
|
|
58
|
-
M & N & O & P --> Q{New observation\nworth saving?}
|
|
59
|
-
Q -- Yes --> R[Write ~/.devcoach/\nlearning-state.md]
|
|
60
|
-
Q -- No --> S
|
|
61
|
-
R --> S([Loop ends])
|
|
62
|
-
```
|
|
63
|
-
|
|
64
|
-
---
|
|
65
|
-
|
|
66
|
-
## Lesson selection
|
|
67
|
-
|
|
68
|
-
When a teachable concept is found, devcoach picks the highest-priority angle
|
|
69
|
-
and calibrates the lesson level to the **per-topic** confidence score — not an average.
|
|
70
|
-
|
|
71
|
-
```mermaid
|
|
72
|
-
flowchart TD
|
|
73
|
-
A([Concepts identified\nin current task]) --> B{Notebook flags\na follow-up angle\nrelevant to this task?}
|
|
74
|
-
B -- Yes --> P1["① Deliver notebook\nfollow-up"]
|
|
75
|
-
|
|
76
|
-
B -- No --> C{Pitfall on a\nprofile topic?}
|
|
77
|
-
C -- Yes --> P2["② Profile pitfall"]
|
|
78
|
-
|
|
79
|
-
C -- No --> D{Interesting pattern\non a profile topic?}
|
|
80
|
-
D -- Yes --> P3["③ Profile pattern"]
|
|
81
|
-
|
|
82
|
-
D -- No --> E{Off-profile concept\nprominent in task?}
|
|
83
|
-
E -- Yes --> P4["④ Off-profile pitfall"]
|
|
84
|
-
|
|
85
|
-
E -- No --> F{Profile topic with\nconfidence < 5?}
|
|
86
|
-
F -- Yes --> P5["⑤ Knowledge gap"]
|
|
87
|
-
|
|
88
|
-
F -- No --> G{Profile topic at\nconfidence 4–6?}
|
|
89
|
-
G -- Yes --> P6["⑥ Deep-dive"]
|
|
90
|
-
|
|
91
|
-
G -- No --> Z([Nothing to teach\n— stay silent])
|
|
92
|
-
|
|
93
|
-
P1 & P2 & P3 & P4 & P5 & P6 --> L[Check taught-topics\nno repeats]
|
|
94
|
-
L --> M{Already taught\nor confidence ≥ 10?}
|
|
95
|
-
M -- Already taught\nnot confidence 10 --> Z
|
|
96
|
-
M -- OK or\nconfidence = 10 --> N[Calibrate level\nper-topic confidence]
|
|
97
|
-
N --> O([Compose and\ndeliver lesson])
|
|
98
|
-
```
|
|
99
|
-
|
|
100
|
-
---
|
|
101
|
-
|
|
102
|
-
## Depth calibration
|
|
103
|
-
|
|
104
|
-
The lesson level is determined by the confidence score for the **specific topic being taught**,
|
|
105
|
-
adjusted by observations in the coaching notebook.
|
|
106
|
-
|
|
107
|
-
| Confidence | Level | Lesson angle |
|
|
108
|
-
|---|---|---|
|
|
109
|
-
| 0 – 3 | Junior | Introduce correct practice, explain from scratch, use analogies |
|
|
110
|
-
| 4 – 6 | Mid | Explain the why, mention trade-offs and alternatives |
|
|
111
|
-
| 7 – 9 | Senior | Edge cases, historical context, architectural implications |
|
|
112
|
-
| 10 | Cutting-edge | Latest developments — ignores level floor and taught-topics filter |
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{devcoach-0.3.9 → devcoach-0.3.10}/docs/screenshots/lesson-postgresql-explain-analyze-dark.png
RENAMED
|
File without changes
|
{devcoach-0.3.9 → devcoach-0.3.10}/docs/screenshots/lesson-postgresql-explain-analyze-light.png
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|