@massu/core 1.3.0 → 1.4.0
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/commands/README.md +23 -11
- package/commands/massu-deploy.python-docker.md +170 -0
- package/commands/massu-deploy.python-fly.md +189 -0
- package/commands/massu-deploy.python-launchd.md +144 -0
- package/commands/massu-deploy.python-systemd.md +163 -0
- package/commands/massu-scaffold-page.swift.md +10 -10
- package/commands/massu-scaffold-router.python-django.md +153 -0
- package/commands/massu-scaffold-router.python-fastapi.md +145 -0
- package/dist/cli.js +9914 -4133
- package/dist/hooks/auto-learning-pipeline.js +45 -2
- package/dist/hooks/classify-failure.js +45 -2
- package/dist/hooks/cost-tracker.js +45 -2
- package/dist/hooks/fix-detector.js +45 -2
- package/dist/hooks/incident-pipeline.js +45 -2
- package/dist/hooks/post-edit-context.js +45 -2
- package/dist/hooks/post-tool-use.js +45 -2
- package/dist/hooks/pre-compact.js +45 -2
- package/dist/hooks/pre-delete-check.js +45 -2
- package/dist/hooks/quality-event.js +45 -2
- package/dist/hooks/rule-enforcement-pipeline.js +45 -2
- package/dist/hooks/session-end.js +45 -2
- package/dist/hooks/session-start.js +4790 -406
- package/dist/hooks/user-prompt.js +45 -2
- package/package.json +13 -4
- package/src/cli.ts +22 -2
- package/src/commands/config-refresh.ts +91 -23
- package/src/commands/init.ts +131 -24
- package/src/commands/install-commands.ts +142 -26
- package/src/commands/refresh-log.ts +37 -0
- package/src/commands/template-engine.ts +260 -0
- package/src/commands/watch.ts +430 -0
- package/src/config.ts +71 -0
- package/src/detect/adapters/nextjs-trpc.ts +166 -0
- package/src/detect/adapters/parse-guard.ts +133 -0
- package/src/detect/adapters/python-django.ts +208 -0
- package/src/detect/adapters/python-fastapi.ts +223 -0
- package/src/detect/adapters/query-helpers.ts +170 -0
- package/src/detect/adapters/runner.ts +252 -0
- package/src/detect/adapters/swift-swiftui.ts +171 -0
- package/src/detect/adapters/tree-sitter-loader.ts +467 -0
- package/src/detect/adapters/types.ts +173 -0
- package/src/detect/codebase-introspector.ts +190 -0
- package/src/detect/index.ts +28 -2
- package/src/detect/migrate.ts +4 -4
- package/src/detect/regex-fallback.ts +449 -0
- package/src/hooks/session-start.ts +94 -3
- package/src/lib/gitToplevel.ts +22 -0
- package/src/lib/installLock.ts +179 -0
- package/src/lib/pidLiveness.ts +67 -0
- package/src/lsp/auto-detect.ts +98 -0
- package/src/lsp/client.ts +776 -0
- package/src/lsp/enrich.ts +127 -0
- package/src/lsp/types.ts +221 -0
- package/src/watch/daemon.ts +385 -0
- package/src/watch/lockfile-detector.ts +65 -0
- package/src/watch/paths.ts +279 -0
- package/src/watch/state.ts +178 -0
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: massu-deploy
|
|
3
|
+
description: "Deploy a Python service supervised by systemd (Linux) — restart the user unit, poll health, tail journalctl, diff before/after, rollback if unhealthy"
|
|
4
|
+
allowed-tools: Bash(*), Read(*), Grep(*), Glob(*)
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Massu Deploy: Python Service — systemd (Linux)
|
|
8
|
+
|
|
9
|
+
Restarts a Python service running under a `systemd --user` unit on Linux. Use this variant when your `massu.config.yaml` declares `config.python.service_label` and your host is Linux.
|
|
10
|
+
|
|
11
|
+
## Workflow Position
|
|
12
|
+
|
|
13
|
+
```
|
|
14
|
+
/massu-push -> /massu-deploy (systemd variant)
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
This command restarts a production service. **If the service handles financial, transactional, or otherwise consequential state, real data is at risk** — pre-flight checks are mandatory.
|
|
18
|
+
|
|
19
|
+
---
|
|
20
|
+
|
|
21
|
+
## NON-NEGOTIABLE RULES
|
|
22
|
+
|
|
23
|
+
- **Never deploy with uncommitted changes** — push first via `/massu-push`
|
|
24
|
+
- **Never deploy with failing tests** — test suite must be green before this runs
|
|
25
|
+
- **Always restart-and-probe** — file-saved ≠ process-running-the-fix
|
|
26
|
+
- **Never kill processes without identifying them first** — confirm the unit name before sending any signal
|
|
27
|
+
|
|
28
|
+
---
|
|
29
|
+
|
|
30
|
+
## Pre-flight
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
# 1. Branch + working tree clean
|
|
34
|
+
test -z "$(git status --porcelain)" || { echo "DIRTY — commit/stash first"; exit 1; }
|
|
35
|
+
|
|
36
|
+
# 2. Tests green
|
|
37
|
+
pytest -x 2>&1 | tail -10
|
|
38
|
+
|
|
39
|
+
# 3. Confirm the unit is active
|
|
40
|
+
systemctl --user status {{config.python.service_label | default("<service-label>")}} --no-pager | head -20
|
|
41
|
+
|
|
42
|
+
# 4. Capture current health for diff
|
|
43
|
+
curl -sS http://localhost:8000/health | python3 -m json.tool \
|
|
44
|
+
> /tmp/{{config.python.service_label | default("service")}}-health-before.json
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
---
|
|
48
|
+
|
|
49
|
+
## Approval Gate
|
|
50
|
+
|
|
51
|
+
```
|
|
52
|
+
===============================================================================
|
|
53
|
+
APPROVAL REQUIRED — SYSTEMD RESTART
|
|
54
|
+
===============================================================================
|
|
55
|
+
|
|
56
|
+
Service unit : {{config.python.service_label | default("<service-label>")}}
|
|
57
|
+
Supervisor : systemd --user (Linux)
|
|
58
|
+
Pre-flight : PASS
|
|
59
|
+
|
|
60
|
+
This will:
|
|
61
|
+
1. systemctl --user restart {{config.python.service_label | default("<service-label>")}}
|
|
62
|
+
2. Poll /health every 2s (max 60s) until 200
|
|
63
|
+
3. Smoke /health + any critical endpoint
|
|
64
|
+
4. Diff before/after health JSON
|
|
65
|
+
5. On failure: print rollback command
|
|
66
|
+
|
|
67
|
+
Reply "approve" or "abort".
|
|
68
|
+
===============================================================================
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
---
|
|
72
|
+
|
|
73
|
+
## Restart
|
|
74
|
+
|
|
75
|
+
```bash
|
|
76
|
+
systemctl --user restart {{config.python.service_label | default("<service-label>")}}
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
If the unit is system-level (not user-level), drop `--user` and run with `sudo`.
|
|
80
|
+
|
|
81
|
+
---
|
|
82
|
+
|
|
83
|
+
## Poll Health
|
|
84
|
+
|
|
85
|
+
```bash
|
|
86
|
+
for i in $(seq 1 30); do
|
|
87
|
+
sleep 2
|
|
88
|
+
status=$(curl -sS -o /dev/null -w "%{http_code}" http://localhost:8000/health || echo "000")
|
|
89
|
+
[ "$status" = "200" ] && { echo "READY after ${i} polls"; break; }
|
|
90
|
+
echo "poll ${i}: ${status}"
|
|
91
|
+
done
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
---
|
|
95
|
+
|
|
96
|
+
## Smoke + Diff
|
|
97
|
+
|
|
98
|
+
```bash
|
|
99
|
+
curl -sS http://localhost:8000/health | python3 -m json.tool \
|
|
100
|
+
> /tmp/{{config.python.service_label | default("service")}}-health-after.json
|
|
101
|
+
|
|
102
|
+
diff \
|
|
103
|
+
/tmp/{{config.python.service_label | default("service")}}-health-before.json \
|
|
104
|
+
/tmp/{{config.python.service_label | default("service")}}-health-after.json \
|
|
105
|
+
| head -50
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
---
|
|
109
|
+
|
|
110
|
+
## Tail Startup Logs
|
|
111
|
+
|
|
112
|
+
```bash
|
|
113
|
+
# Recent errors from the unit (2-minute window)
|
|
114
|
+
journalctl --user -u {{config.python.service_label | default("<service-label>")}} \
|
|
115
|
+
--since "2 minutes ago" --no-pager | grep -iE "error|warn" | head -20
|
|
116
|
+
|
|
117
|
+
# Follow live (Ctrl-C to stop)
|
|
118
|
+
journalctl --user -u {{config.python.service_label | default("<service-label>")}} -f --no-pager
|
|
119
|
+
|
|
120
|
+
# Full boot for this unit
|
|
121
|
+
journalctl --user -u {{config.python.service_label | default("<service-label>")}} \
|
|
122
|
+
-b --no-pager | tail -100
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
---
|
|
126
|
+
|
|
127
|
+
## Rollback
|
|
128
|
+
|
|
129
|
+
If `/health` does not return 200 within 60s, or any smoke check fails:
|
|
130
|
+
|
|
131
|
+
```bash
|
|
132
|
+
git revert HEAD --no-edit
|
|
133
|
+
systemctl --user restart {{config.python.service_label | default("<service-label>")}}
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
Then page yourself or your on-call — an unhealthy production service is an incident.
|
|
137
|
+
|
|
138
|
+
---
|
|
139
|
+
|
|
140
|
+
## Useful systemd Diagnostics
|
|
141
|
+
|
|
142
|
+
```bash
|
|
143
|
+
# Check if the unit file needs a daemon-reload after editing the .service file
|
|
144
|
+
systemctl --user daemon-reload
|
|
145
|
+
|
|
146
|
+
# Show the resolved unit definition (useful to verify ExecStart path)
|
|
147
|
+
systemctl --user cat {{config.python.service_label | default("<service-label>")}}
|
|
148
|
+
|
|
149
|
+
# List recent restarts / crash history
|
|
150
|
+
systemctl --user show {{config.python.service_label | default("<service-label>")}} \
|
|
151
|
+
--property=NRestarts,ActiveState,SubState
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
---
|
|
155
|
+
|
|
156
|
+
## Audit Log
|
|
157
|
+
|
|
158
|
+
```bash
|
|
159
|
+
echo "$(date -u +%FT%TZ) deploy surface=systemd sha=$(git rev-parse HEAD) unit={{config.python.service_label | default("<service-label>")}} actor=$(whoami)" \
|
|
160
|
+
>> data/audit/deploys.log
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
Done. Report: unit name, sha, restart time, health status, any warnings.
|
|
@@ -12,11 +12,11 @@ Creates a SwiftUI View + `@MainActor` ViewModel + Decodable response model. Suit
|
|
|
12
12
|
|
|
13
13
|
| File | Purpose |
|
|
14
14
|
|------|---------|
|
|
15
|
-
|
|
|
16
|
-
|
|
|
17
|
-
|
|
|
15
|
+
| `{{paths.swift_source | default("ios/Sources")}}/Features/<feature>/Views/<Name>View.swift` | SwiftUI view |
|
|
16
|
+
| `{{paths.swift_source | default("ios/Sources")}}/Features/<feature>/ViewModels/<Name>ViewModel.swift` | `@MainActor` ObservableObject |
|
|
17
|
+
| `{{paths.swift_source | default("ios/Sources")}}/Features/<feature>/Models/<Name>Response.swift` | Decodable matching API contract |
|
|
18
18
|
|
|
19
|
-
> **Path resolution**: substitute
|
|
19
|
+
> **Path resolution**: substitute `{{paths.swift_source | default("ios/Sources")}}` against your project's `massu.config.yaml` (`paths.swift_source`). If unset, fall back to whatever the project already uses (`Sources/`, `apps/ios/<App>/<App>/`, etc.).
|
|
20
20
|
|
|
21
21
|
## Template — `<Name>View.swift`
|
|
22
22
|
|
|
@@ -58,10 +58,10 @@ final class <Name>ViewModel: ObservableObject {
|
|
|
58
58
|
@Published var isLoading = false
|
|
59
59
|
@Published var error: String?
|
|
60
60
|
|
|
61
|
-
// Substitute APIClient with the project's actual API wrapper.
|
|
62
|
-
private let api: APIClient
|
|
61
|
+
// Substitute {{detected.swift.api_client_class | default("APIClient")}} with the project's actual API wrapper.
|
|
62
|
+
private let api: {{detected.swift.api_client_class | default("APIClient")}}
|
|
63
63
|
|
|
64
|
-
init(api: APIClient = .shared) {
|
|
64
|
+
init(api: {{detected.swift.api_client_class | default("APIClient")}} = .shared) {
|
|
65
65
|
self.api = api
|
|
66
66
|
}
|
|
67
67
|
|
|
@@ -97,7 +97,7 @@ struct <Name>Response: Decodable {
|
|
|
97
97
|
|
|
98
98
|
- **Decodable silent nil**: with `JSONDecoder.keyDecodingStrategy = .convertFromSnakeCase`, a typo'd property decodes to nil with NO error. Hand-verify every property against a real API response — entire screens have shipped showing dead data because of this.
|
|
99
99
|
- **`.system(size:weight:design:)` argument order**: weight before design. Reversed args silently fall back to default font.
|
|
100
|
-
- **Biometric authentication**: for sensitive actions, use `LAPolicy.deviceOwnerAuthenticationWithBiometrics` — NOT `deviceOwnerAuthentication` (which falls back to a passcode and defeats the gate).
|
|
100
|
+
- **Biometric authentication**: for sensitive actions, use `LAPolicy.{{detected.swift.biometric_policy | default("deviceOwnerAuthenticationWithBiometrics")}}` — NOT `deviceOwnerAuthentication` (which falls back to a passcode and defeats the gate).
|
|
101
101
|
- **Sheet state**: never clear `@State` sheet-bound vars in async callbacks; use `.onDismiss` instead.
|
|
102
102
|
- **XcodeGen target naming**: cross-platform projects often split iOS / visionOS into separate targets (e.g., `<App>_iOS` / `<App>_visionOS`). Build the platform-specific scheme, NOT the umbrella name.
|
|
103
103
|
- **`@MainActor` on view models**: any `@Published` field that drives UI must be set on the main actor. Async work updates state via `await MainActor.run { ... }` if the function isn't already main-actor-isolated.
|
|
@@ -107,10 +107,10 @@ struct <Name>Response: Decodable {
|
|
|
107
107
|
1. Ask: which feature folder? Which target (iOS / visionOS / both)?
|
|
108
108
|
2. Read the API endpoint's actual JSON response (e.g., `curl -sS http://<service>/api/<endpoint> | python3 -m json.tool`) — copy the EXACT key names so the Decodable can't drift.
|
|
109
109
|
3. Write the three files.
|
|
110
|
-
4. Add the new files to your project's manifest (`project.yml` for XcodeGen, or directly in Xcode for hand-managed projects); regen if needed: `cd
|
|
110
|
+
4. Add the new files to your project's manifest (`project.yml` for XcodeGen, or directly in Xcode for hand-managed projects); regen if needed: `cd {{paths.swift_source | default("ios/Sources")}}/.. && xcodegen`.
|
|
111
111
|
5. Build the right scheme:
|
|
112
112
|
```bash
|
|
113
|
-
cd
|
|
113
|
+
cd {{paths.swift_source | default("ios/Sources")}}/.. && xcodebuild -scheme <Target>_iOS -destination 'generic/platform=iOS Simulator' build | tail -20
|
|
114
114
|
```
|
|
115
115
|
|
|
116
116
|
## START NOW
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: massu-scaffold-router
|
|
3
|
+
description: "Django-specific scaffold for {{paths.python_source | default("django_app")}}/views.py — creates function-based and class-based views with login_required, plus urls.py registration"
|
|
4
|
+
allowed-tools: Bash(*), Read(*), Write(*), Edit(*), Grep(*), Glob(*)
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Scaffold New Django View
|
|
8
|
+
|
|
9
|
+
Creates Django views in `{{paths.python_source | default("django_app")}}` following the project's conventions. Covers function-based views (FBV), class-based views (CBV), and the corresponding `urls.py` registration. Auth guard uses `{{detected.python.auth_dep | default("login_required")}}`.
|
|
10
|
+
|
|
11
|
+
## What Gets Created
|
|
12
|
+
|
|
13
|
+
| File | Purpose |
|
|
14
|
+
|------|---------|
|
|
15
|
+
| `{{paths.python_source | default("django_app")}}/views.py` | FBV + CBV examples |
|
|
16
|
+
| `{{paths.python_source | default("django_app")}}/urls.py` | URL routing registration |
|
|
17
|
+
| `{{paths.python_test | default("tests")}}/test_<name>_views.py` | View tests (auth + happy path) |
|
|
18
|
+
|
|
19
|
+
> **Auth decorator**: this template uses `{{detected.python.auth_dep | default("login_required")}}` — sourced from the massu introspector. If your project uses a custom decorator or `@permission_required`, swap it in.
|
|
20
|
+
|
|
21
|
+
## Template — Function-Based View
|
|
22
|
+
|
|
23
|
+
```python
|
|
24
|
+
"""<Name> views — describe purpose in one line."""
|
|
25
|
+
|
|
26
|
+
import logging
|
|
27
|
+
|
|
28
|
+
from django.contrib.auth.decorators import login_required
|
|
29
|
+
from django.http import HttpRequest, JsonResponse
|
|
30
|
+
from django.views.decorators.http import require_http_methods
|
|
31
|
+
|
|
32
|
+
# Use the detected auth decorator if your project wraps the Django built-in.
|
|
33
|
+
# from .auth import {{detected.python.auth_dep | default("login_required")}}
|
|
34
|
+
|
|
35
|
+
logger = logging.getLogger(__name__)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@{{detected.python.auth_dep | default("login_required")}}
|
|
39
|
+
@require_http_methods(["GET"])
|
|
40
|
+
def list_items(request: HttpRequest) -> JsonResponse:
|
|
41
|
+
"""List items. Read-only — login guard is sufficient."""
|
|
42
|
+
return JsonResponse({"ok": True, "items": []})
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
@{{detected.python.auth_dep | default("login_required")}}
|
|
46
|
+
@require_http_methods(["POST"])
|
|
47
|
+
def create_item(request: HttpRequest) -> JsonResponse:
|
|
48
|
+
"""Create an item. Mutating — ensure the auth decorator enforces the right role."""
|
|
49
|
+
# Validate POST body here — never trust raw request.POST for numeric / typed fields.
|
|
50
|
+
logger.info("item created by user=%s", request.user.pk)
|
|
51
|
+
return JsonResponse({"ok": True})
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
## Template — Class-Based View
|
|
55
|
+
|
|
56
|
+
```python
|
|
57
|
+
from django.contrib.auth.mixins import LoginRequiredMixin
|
|
58
|
+
from django.views import View
|
|
59
|
+
from django.http import HttpRequest, JsonResponse
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
class ItemListView(LoginRequiredMixin, View):
|
|
63
|
+
"""Class-based list view. LoginRequiredMixin redirects unauthenticated users."""
|
|
64
|
+
|
|
65
|
+
def get(self, request: HttpRequest) -> JsonResponse:
|
|
66
|
+
return JsonResponse({"ok": True, "items": []})
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
class ItemDetailView(LoginRequiredMixin, View):
|
|
70
|
+
def get(self, request: HttpRequest, pk: int) -> JsonResponse:
|
|
71
|
+
# Fetch from DB; raise Http404 if not found.
|
|
72
|
+
return JsonResponse({"ok": True, "id": pk})
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
## Template — `urls.py` Registration
|
|
76
|
+
|
|
77
|
+
```python
|
|
78
|
+
from django.urls import path
|
|
79
|
+
from . import views
|
|
80
|
+
|
|
81
|
+
app_name = "<name>"
|
|
82
|
+
|
|
83
|
+
urlpatterns = [
|
|
84
|
+
path("items/", views.list_items, name="list"),
|
|
85
|
+
path("items/create/", views.create_item, name="create"),
|
|
86
|
+
# CBV registration:
|
|
87
|
+
path("items/<int:pk>/", views.ItemDetailView.as_view(), name="detail"),
|
|
88
|
+
]
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
Then include in the project's root `urls.py`:
|
|
92
|
+
|
|
93
|
+
```python
|
|
94
|
+
from django.urls import path, include
|
|
95
|
+
|
|
96
|
+
urlpatterns = [
|
|
97
|
+
# ...
|
|
98
|
+
path("api/<name>/", include("<app_label>.urls")),
|
|
99
|
+
]
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
## Test scaffold (`{{paths.python_test | default("tests")}}/test_<name>_views.py`)
|
|
103
|
+
|
|
104
|
+
```python
|
|
105
|
+
import pytest
|
|
106
|
+
from django.test import Client
|
|
107
|
+
from django.contrib.auth import get_user_model
|
|
108
|
+
|
|
109
|
+
User = get_user_model()
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
@pytest.mark.django_db
|
|
113
|
+
def test_list_items_requires_auth(client: Client):
|
|
114
|
+
response = client.get("/api/<name>/items/")
|
|
115
|
+
# login_required redirects; DRF returns 403 — accept either.
|
|
116
|
+
assert response.status_code in (302, 401, 403)
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
@pytest.mark.django_db
|
|
120
|
+
def test_list_items_authenticated(client: Client, django_user_model):
|
|
121
|
+
user = django_user_model.objects.create_user(username="tester", password="pass")
|
|
122
|
+
client.force_login(user)
|
|
123
|
+
response = client.get("/api/<name>/items/")
|
|
124
|
+
assert response.status_code == 200
|
|
125
|
+
data = response.json()
|
|
126
|
+
assert data["ok"] is True
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
## Django Conventions
|
|
130
|
+
|
|
131
|
+
- **Always guard mutating views** with `{{detected.python.auth_dep | default("login_required")}}` (or a role/permission mixin for sensitive operations).
|
|
132
|
+
- **Use `LoginRequiredMixin` for CBVs** — decorator-only auth on CBVs can be bypassed via HTTP method routing.
|
|
133
|
+
- **Never trust `request.POST` for typed fields** — validate with a Django Form or DRF serializer.
|
|
134
|
+
- **Atomic DB writes** — wrap multi-step mutations in `django.db.transaction.atomic`.
|
|
135
|
+
- **CSRF** — `@require_http_methods` does NOT exempt CSRF. For JSON APIs, either use DRF's `CSRFExemptSessionAuthentication` or enforce the CSRF token client-side.
|
|
136
|
+
- **Avoid N+1** — use `select_related` / `prefetch_related` in list views.
|
|
137
|
+
|
|
138
|
+
## Process
|
|
139
|
+
|
|
140
|
+
1. Ask user: which Django app (`app_label`)? What URL prefix? What views are needed?
|
|
141
|
+
2. Confirm path: `{{paths.python_source | default("django_app")}}/views.py`.
|
|
142
|
+
3. Write or append to `views.py`; write the `urls.py` snippet.
|
|
143
|
+
4. Include the URL conf in the project root `urls.py`.
|
|
144
|
+
5. Run migrations if the new views touch new models: `python manage.py makemigrations && python manage.py migrate`.
|
|
145
|
+
6. Smoke: `python manage.py runserver` and curl or use the test client.
|
|
146
|
+
|
|
147
|
+
## START NOW
|
|
148
|
+
|
|
149
|
+
Ask the user:
|
|
150
|
+
1. Which Django app (label) owns these views?
|
|
151
|
+
2. What URL prefix? (e.g. `api/<name>/`)
|
|
152
|
+
3. Function-based or class-based views (or both)?
|
|
153
|
+
4. Does any view touch sensitive state? — that decides the auth decorator vs mixin.
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: massu-scaffold-router
|
|
3
|
+
description: "FastAPI-specific scaffold for {{paths.python_source | default("app")}}/routers/ — creates the router file, registers it in main.py, adds Pydantic schemas, and applies the project's detected auth dependency"
|
|
4
|
+
allowed-tools: Bash(*), Read(*), Write(*), Edit(*), Grep(*), Glob(*)
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Scaffold New FastAPI Router
|
|
8
|
+
|
|
9
|
+
Creates a complete FastAPI router following the project's existing conventions in `{{paths.python_source | default("app")}}/routers/`. Auth dependency sourced from the massu introspector's `detected.python.auth_dep` (falls back to `get_current_user` if not detected).
|
|
10
|
+
|
|
11
|
+
## What Gets Created
|
|
12
|
+
|
|
13
|
+
| File | Purpose |
|
|
14
|
+
|------|---------|
|
|
15
|
+
| `{{paths.python_source | default("app")}}/routers/<name>.py` | Router with endpoints |
|
|
16
|
+
| Registration in `{{paths.python_source | default("app")}}/main.py` | `app.include_router(...)` |
|
|
17
|
+
| `{{paths.python_test | default("tests")}}/test_<name>_router.py` | Router test (auth + happy path + error path) |
|
|
18
|
+
|
|
19
|
+
> **Auth dependency**: this template uses `{{detected.python.auth_dep | default("get_current_user")}}` — the value introspected from your codebase by massu. If your project uses a different dependency, adjust the import and `Depends(...)` call.
|
|
20
|
+
|
|
21
|
+
> **Path resolution**: `paths.python_source` and `paths.python_test` come from `massu.config.yaml`. If those keys are not declared, the fallbacks `app/` and `tests/` are used.
|
|
22
|
+
|
|
23
|
+
## Template
|
|
24
|
+
|
|
25
|
+
```python
|
|
26
|
+
"""<Name> API — describe purpose in one line.
|
|
27
|
+
|
|
28
|
+
Plan: <plan-id> if applicable. Owner: <subsystem>.
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
import logging
|
|
32
|
+
|
|
33
|
+
from fastapi import APIRouter, Depends, HTTPException, Request
|
|
34
|
+
from pydantic import BaseModel, Field
|
|
35
|
+
|
|
36
|
+
# Auth dependency detected from your codebase.
|
|
37
|
+
from ._shared import {{detected.python.auth_dep | default("get_current_user")}}
|
|
38
|
+
# from ..auth import require_role # for endpoints that mutate sensitive state
|
|
39
|
+
|
|
40
|
+
logger = logging.getLogger(__name__)
|
|
41
|
+
|
|
42
|
+
router = APIRouter(prefix="{{detected.python.api_prefix_base | default("/api")}}/<name>", tags=["<name>"])
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class FooRequest(BaseModel):
|
|
46
|
+
symbol: str = Field(min_length=1, max_length=16)
|
|
47
|
+
# ALWAYS bound numeric inputs — open-ended floats are how unit-mismatch and
|
|
48
|
+
# APY-style overflow bugs sneak through.
|
|
49
|
+
quantity: float = Field(gt=0, le=1_000_000)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class FooResponse(BaseModel):
|
|
53
|
+
ok: bool
|
|
54
|
+
payload: dict
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
@router.get("/items")
|
|
58
|
+
async def list_items(
|
|
59
|
+
request: Request,
|
|
60
|
+
user: dict = Depends({{detected.python.auth_dep | default("get_current_user")}}),
|
|
61
|
+
) -> FooResponse:
|
|
62
|
+
"""List items. Read-only — base auth dependency is sufficient."""
|
|
63
|
+
# Async-only I/O. Wrap external calls with `async with asyncio.timeout(N)`;
|
|
64
|
+
# client-library timeouts (httpx, aiohttp) alone do not cover DNS/TLS hangs.
|
|
65
|
+
return FooResponse(ok=True, payload={"items": []})
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
@router.post("/orders")
|
|
69
|
+
async def create_order(
|
|
70
|
+
body: FooRequest,
|
|
71
|
+
request: Request,
|
|
72
|
+
user: dict = Depends({{detected.python.auth_dep | default("get_current_user")}}), # SWAP for require_role(...) for any state-mutating action
|
|
73
|
+
) -> FooResponse:
|
|
74
|
+
"""Mutating endpoint — enforce role-based auth (or service-token) here."""
|
|
75
|
+
logger.info("order created symbol=%s qty=%s user=%s", body.symbol, body.quantity, user.get("user_id"))
|
|
76
|
+
return FooResponse(ok=True, payload={"symbol": body.symbol, "qty": body.quantity})
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
## Registration in `main.py`
|
|
80
|
+
|
|
81
|
+
```python
|
|
82
|
+
# at top of file with other router imports
|
|
83
|
+
from .routers.<name> import router as <name>_router
|
|
84
|
+
|
|
85
|
+
# in the section where other routers are included
|
|
86
|
+
app.include_router(<name>_router)
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
## Test scaffold (`{{paths.python_test | default("tests")}}/test_<name>_router.py`)
|
|
90
|
+
|
|
91
|
+
```python
|
|
92
|
+
import pytest
|
|
93
|
+
from httpx import AsyncClient, ASGITransport
|
|
94
|
+
|
|
95
|
+
from <project_package>.main import app # substitute your top-level package
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
@pytest.mark.asyncio
|
|
99
|
+
async def test_list_items_requires_auth():
|
|
100
|
+
async with AsyncClient(transport=ASGITransport(app=app), base_url="http://test") as ac:
|
|
101
|
+
r = await ac.get("{{detected.python.api_prefix_base | default("/api")}}/<name>/items")
|
|
102
|
+
assert r.status_code in (401, 403)
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
@pytest.mark.asyncio
|
|
106
|
+
async def test_create_order_input_validation(auth_headers):
|
|
107
|
+
async with AsyncClient(transport=ASGITransport(app=app), base_url="http://test") as ac:
|
|
108
|
+
r = await ac.post("{{detected.python.api_prefix_base | default("/api")}}/<name>/orders", json={"symbol": "", "quantity": 0}, headers=auth_headers)
|
|
109
|
+
assert r.status_code == 422
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
## FastAPI Conventions (apply in any project)
|
|
113
|
+
|
|
114
|
+
- **Auth choice**:
|
|
115
|
+
- `Depends({{detected.python.auth_dep | default("get_current_user")}})` for read-only endpoints
|
|
116
|
+
- A role-gated dependency (e.g. `require_role("admin")`) for ANY state-mutating endpoint — non-negotiable for safety-critical surfaces
|
|
117
|
+
- Service-token / machine-auth checks happen BEFORE any "auth disabled" dev bypass
|
|
118
|
+
- **Async only**: every I/O call uses `async def` + `await`. Wrap external calls in `async with asyncio.timeout(N)` — internal client-library timeouts are not enough for DNS/TLS hangs.
|
|
119
|
+
- **Bound numeric inputs**: `Field(gt=..., le=...)` on every numeric. Open-ended ranges produce overflow bugs in financial / metric / quantity contexts.
|
|
120
|
+
- **Validate input strings**: symbols, IDs, slugs — use a project-local validator, never trust raw `str` for downstream lookups.
|
|
121
|
+
- **No hardcoded sentinel values**: `0`, `0.00`, `""`, `None` are usually indistinguishable from real values. Be deliberate.
|
|
122
|
+
- **Module-level state**: locks must be lazy (`asyncio.Lock()` at module top binds to the wrong loop). Background `create_task()` returns must be stored in a module-level set with `add_done_callback` to prevent GC.
|
|
123
|
+
- **Silent drops log WARNING** — never DEBUG. Anything dropped at scale must be visible in production logs.
|
|
124
|
+
- **Dependency injection**: any class that touches sensitive state (trades, memory, billing) should accept its dependencies via constructor or `set_*` — never assume singleton is pre-wired.
|
|
125
|
+
|
|
126
|
+
## Process
|
|
127
|
+
|
|
128
|
+
1. Ask user: what subsystem owns this router? What endpoints does it need? Which are read-only vs mutating?
|
|
129
|
+
2. Confirm path: `{{paths.python_source | default("app")}}/routers/<name>.py`. If the name collides with an existing router, stop and ask.
|
|
130
|
+
3. Write the router file using the template above; pick the right auth dependency per endpoint.
|
|
131
|
+
4. Write the test scaffold and confirm it imports cleanly: `pytest {{paths.python_test | default("tests")}}/test_<name>_router.py -x --collect-only`.
|
|
132
|
+
5. Add the `app.include_router(<name>_router)` line to `main.py` AFTER the file exists (split-commit safety).
|
|
133
|
+
6. Verify route registration:
|
|
134
|
+
```bash
|
|
135
|
+
python -c "from <project_package>.main import app; print([r.path for r in app.routes if '/api/<name>' in str(r.path)])"
|
|
136
|
+
```
|
|
137
|
+
7. Restart the service and curl-smoke the new endpoint (tests passing ≠ running process has the change).
|
|
138
|
+
|
|
139
|
+
## START NOW
|
|
140
|
+
|
|
141
|
+
Ask the user:
|
|
142
|
+
1. What subsystem/feature owns this router?
|
|
143
|
+
2. What's the URL prefix? (default: `{{detected.python.api_prefix_base | default("/api")}}/<name>`)
|
|
144
|
+
3. What endpoints, and which are mutating vs read-only?
|
|
145
|
+
4. Does any endpoint touch sensitive state (trades, billing, user permissions)? That decides whether to use a role-gated dependency vs the base auth dependency.
|