@massu/core 1.3.0 → 1.4.0-soak.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.
Files changed (56) hide show
  1. package/commands/README.md +23 -8
  2. package/commands/massu-deploy.python-docker.md +170 -0
  3. package/commands/massu-deploy.python-fly.md +189 -0
  4. package/commands/massu-deploy.python-launchd.md +144 -0
  5. package/commands/massu-deploy.python-systemd.md +163 -0
  6. package/commands/massu-scaffold-page.swift.md +10 -10
  7. package/commands/massu-scaffold-router.python-django.md +153 -0
  8. package/commands/massu-scaffold-router.python-fastapi.md +145 -0
  9. package/dist/cli.js +9906 -4133
  10. package/dist/hooks/auto-learning-pipeline.js +37 -2
  11. package/dist/hooks/classify-failure.js +37 -2
  12. package/dist/hooks/cost-tracker.js +37 -2
  13. package/dist/hooks/fix-detector.js +37 -2
  14. package/dist/hooks/incident-pipeline.js +37 -2
  15. package/dist/hooks/post-edit-context.js +37 -2
  16. package/dist/hooks/post-tool-use.js +37 -2
  17. package/dist/hooks/pre-compact.js +37 -2
  18. package/dist/hooks/pre-delete-check.js +37 -2
  19. package/dist/hooks/quality-event.js +37 -2
  20. package/dist/hooks/rule-enforcement-pipeline.js +37 -2
  21. package/dist/hooks/session-end.js +37 -2
  22. package/dist/hooks/session-start.js +4782 -406
  23. package/dist/hooks/user-prompt.js +37 -2
  24. package/package.json +10 -4
  25. package/src/cli.ts +22 -2
  26. package/src/commands/config-refresh.ts +88 -20
  27. package/src/commands/init.ts +130 -23
  28. package/src/commands/install-commands.ts +142 -26
  29. package/src/commands/refresh-log.ts +37 -0
  30. package/src/commands/template-engine.ts +262 -0
  31. package/src/commands/watch.ts +430 -0
  32. package/src/config.ts +63 -0
  33. package/src/detect/adapters/nextjs-trpc.ts +166 -0
  34. package/src/detect/adapters/parse-guard.ts +133 -0
  35. package/src/detect/adapters/python-django.ts +208 -0
  36. package/src/detect/adapters/python-fastapi.ts +223 -0
  37. package/src/detect/adapters/query-helpers.ts +170 -0
  38. package/src/detect/adapters/runner.ts +252 -0
  39. package/src/detect/adapters/swift-swiftui.ts +171 -0
  40. package/src/detect/adapters/tree-sitter-loader.ts +348 -0
  41. package/src/detect/adapters/types.ts +174 -0
  42. package/src/detect/codebase-introspector.ts +190 -0
  43. package/src/detect/index.ts +28 -2
  44. package/src/detect/regex-fallback.ts +449 -0
  45. package/src/hooks/session-start.ts +94 -3
  46. package/src/lib/gitToplevel.ts +22 -0
  47. package/src/lib/installLock.ts +179 -0
  48. package/src/lib/pidLiveness.ts +67 -0
  49. package/src/lsp/auto-detect.ts +89 -0
  50. package/src/lsp/client.ts +590 -0
  51. package/src/lsp/enrich.ts +127 -0
  52. package/src/lsp/types.ts +221 -0
  53. package/src/watch/daemon.ts +385 -0
  54. package/src/watch/lockfile-detector.ts +65 -0
  55. package/src/watch/paths.ts +279 -0
  56. 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
- | `${paths.swift_source}/Features/<feature>/Views/<Name>View.swift` | SwiftUI view |
16
- | `${paths.swift_source}/Features/<feature>/ViewModels/<Name>ViewModel.swift` | `@MainActor` ObservableObject |
17
- | `${paths.swift_source}/Features/<feature>/Models/<Name>Response.swift` | Decodable matching API contract |
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 `${paths.swift_source}` 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.).
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 ${paths.swift_source}/.. && xcodegen`.
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 ${paths.swift_source}/.. && xcodebuild -scheme <Target>_iOS -destination 'generic/platform=iOS Simulator' build | tail -20
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.