@massu/core 1.2.1 → 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 (61) hide show
  1. package/README.md +40 -0
  2. package/commands/README.md +137 -0
  3. package/commands/massu-deploy.python-docker.md +170 -0
  4. package/commands/massu-deploy.python-fly.md +189 -0
  5. package/commands/massu-deploy.python-launchd.md +144 -0
  6. package/commands/massu-deploy.python-systemd.md +163 -0
  7. package/commands/massu-deploy.python.md +200 -0
  8. package/commands/massu-scaffold-page.md +172 -59
  9. package/commands/massu-scaffold-page.swift.md +121 -0
  10. package/commands/massu-scaffold-router.python-django.md +153 -0
  11. package/commands/massu-scaffold-router.python-fastapi.md +145 -0
  12. package/commands/massu-scaffold-router.python.md +143 -0
  13. package/dist/cli.js +10170 -4138
  14. package/dist/hooks/auto-learning-pipeline.js +44 -6
  15. package/dist/hooks/classify-failure.js +44 -6
  16. package/dist/hooks/cost-tracker.js +44 -6
  17. package/dist/hooks/fix-detector.js +44 -6
  18. package/dist/hooks/incident-pipeline.js +44 -6
  19. package/dist/hooks/post-edit-context.js +44 -6
  20. package/dist/hooks/post-tool-use.js +44 -6
  21. package/dist/hooks/pre-compact.js +44 -6
  22. package/dist/hooks/pre-delete-check.js +44 -6
  23. package/dist/hooks/quality-event.js +44 -6
  24. package/dist/hooks/rule-enforcement-pipeline.js +44 -6
  25. package/dist/hooks/session-end.js +44 -6
  26. package/dist/hooks/session-start.js +4789 -410
  27. package/dist/hooks/user-prompt.js +44 -6
  28. package/package.json +10 -4
  29. package/src/cli.ts +28 -2
  30. package/src/commands/config-refresh.ts +88 -20
  31. package/src/commands/init.ts +130 -23
  32. package/src/commands/install-commands.ts +482 -42
  33. package/src/commands/refresh-log.ts +37 -0
  34. package/src/commands/show-template.ts +65 -0
  35. package/src/commands/template-engine.ts +262 -0
  36. package/src/commands/watch.ts +430 -0
  37. package/src/config.ts +69 -3
  38. package/src/detect/adapters/nextjs-trpc.ts +166 -0
  39. package/src/detect/adapters/parse-guard.ts +133 -0
  40. package/src/detect/adapters/python-django.ts +208 -0
  41. package/src/detect/adapters/python-fastapi.ts +223 -0
  42. package/src/detect/adapters/query-helpers.ts +170 -0
  43. package/src/detect/adapters/runner.ts +252 -0
  44. package/src/detect/adapters/swift-swiftui.ts +171 -0
  45. package/src/detect/adapters/tree-sitter-loader.ts +348 -0
  46. package/src/detect/adapters/types.ts +174 -0
  47. package/src/detect/codebase-introspector.ts +190 -0
  48. package/src/detect/index.ts +28 -2
  49. package/src/detect/regex-fallback.ts +449 -0
  50. package/src/hooks/session-start.ts +94 -3
  51. package/src/lib/gitToplevel.ts +22 -0
  52. package/src/lib/installLock.ts +179 -0
  53. package/src/lib/pidLiveness.ts +67 -0
  54. package/src/lsp/auto-detect.ts +89 -0
  55. package/src/lsp/client.ts +590 -0
  56. package/src/lsp/enrich.ts +127 -0
  57. package/src/lsp/types.ts +221 -0
  58. package/src/watch/daemon.ts +385 -0
  59. package/src/watch/lockfile-detector.ts +65 -0
  60. package/src/watch/paths.ts +279 -0
  61. package/src/watch/state.ts +178 -0
@@ -0,0 +1,121 @@
1
+ ---
2
+ name: massu-scaffold-page
3
+ description: "When user wants to create a new view, screen, or page in a SwiftUI iOS / visionOS app — scaffolds the View, ViewModel, and Decodable response model with project conventions"
4
+ allowed-tools: Bash(*), Read(*), Write(*), Edit(*), Grep(*), Glob(*)
5
+ ---
6
+
7
+ # Scaffold New SwiftUI View
8
+
9
+ Creates a SwiftUI View + `@MainActor` ViewModel + Decodable response model. Suitable for iOS, visionOS, or any cross-platform SwiftUI target.
10
+
11
+ ## What Gets Created
12
+
13
+ | File | Purpose |
14
+ |------|---------|
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
+
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
+
21
+ ## Template — `<Name>View.swift`
22
+
23
+ ```swift
24
+ import SwiftUI
25
+
26
+ struct <Name>View: View {
27
+ @StateObject private var viewModel = <Name>ViewModel()
28
+
29
+ var body: some View {
30
+ Group {
31
+ if viewModel.isLoading {
32
+ ProgressView()
33
+ } else if let error = viewModel.error {
34
+ ErrorState(message: error) { Task { await viewModel.load() } }
35
+ } else {
36
+ content
37
+ }
38
+ }
39
+ .task { await viewModel.load() }
40
+ .navigationTitle("<Title>")
41
+ }
42
+
43
+ private var content: some View {
44
+ // Build the real view here
45
+ EmptyView()
46
+ }
47
+ }
48
+ ```
49
+
50
+ ## Template — `<Name>ViewModel.swift`
51
+
52
+ ```swift
53
+ import Foundation
54
+
55
+ @MainActor
56
+ final class <Name>ViewModel: ObservableObject {
57
+ @Published var data: <Name>Response?
58
+ @Published var isLoading = false
59
+ @Published var error: String?
60
+
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
+
64
+ init(api: {{detected.swift.api_client_class | default("APIClient")}} = .shared) {
65
+ self.api = api
66
+ }
67
+
68
+ func load() async {
69
+ isLoading = true
70
+ error = nil
71
+ defer { isLoading = false }
72
+ do {
73
+ data = try await api.get("/api/<endpoint>", as: <Name>Response.self)
74
+ } catch {
75
+ self.error = humanReadableError(error)
76
+ }
77
+ }
78
+ }
79
+ ```
80
+
81
+ ## Template — `<Name>Response.swift`
82
+
83
+ ```swift
84
+ import Foundation
85
+
86
+ struct <Name>Response: Decodable {
87
+ // IMPORTANT: properties MUST be camelCase versions of the snake_case API keys.
88
+ // The decoder typically uses .convertFromSnakeCase, but mismatches decode to
89
+ // nil silently — verify property names against an actual API response.
90
+ let symbol: String
91
+ let priceUsd: Double // matches "price_usd"
92
+ let updatedAt: Date // matches "updated_at"
93
+ }
94
+ ```
95
+
96
+ ## SwiftUI Conventions (apply in any project)
97
+
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
+ - **`.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.{{detected.swift.biometric_policy | default("deviceOwnerAuthenticationWithBiometrics")}}` — NOT `deviceOwnerAuthentication` (which falls back to a passcode and defeats the gate).
101
+ - **Sheet state**: never clear `@State` sheet-bound vars in async callbacks; use `.onDismiss` instead.
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
+ - **`@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.
104
+
105
+ ## Process
106
+
107
+ 1. Ask: which feature folder? Which target (iOS / visionOS / both)?
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
+ 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 | default("ios/Sources")}}/.. && xcodegen`.
111
+ 5. Build the right scheme:
112
+ ```bash
113
+ cd {{paths.swift_source | default("ios/Sources")}}/.. && xcodebuild -scheme <Target>_iOS -destination 'generic/platform=iOS Simulator' build | tail -20
114
+ ```
115
+
116
+ ## START NOW
117
+
118
+ Ask the user:
119
+ 1. Which feature folder, and which target (iOS / visionOS / both)?
120
+ 2. What does the screen show, and which API endpoint feeds it?
121
+ 3. Does it perform any sensitive action (purchase, trade, settings change)? — if yes, the project's biometric gate is required.
@@ -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.
@@ -0,0 +1,143 @@
1
+ ---
2
+ name: massu-scaffold-router
3
+ description: "When user wants to create a new FastAPI router, API endpoint, or backend procedure — scaffolds the router file, registers it in main.py, adds Pydantic schemas, applies project auth dependencies"
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}/routers/` (or wherever your project's `paths.python_source` resolves).
10
+
11
+ ## What Gets Created
12
+
13
+ | File | Purpose |
14
+ |------|---------|
15
+ | `${paths.python_source}/routers/<name>.py` | Router with endpoints |
16
+ | Registration in `${paths.python_source}/main.py` | `app.include_router(...)` |
17
+ | `${paths.python_test}/test_<name>_router.py` | Router test (auth + happy path + error path) |
18
+
19
+ > **Path resolution**: substitute the placeholders against your project's `massu.config.yaml` (`paths.python_source`, `paths.python_test`). If those keys are not declared, fall back to the conventional `app/`, `apps/<service>/<package>/`, or `src/` structure your project uses today.
20
+
21
+ ## Template
22
+
23
+ ```python
24
+ """<Name> API — describe purpose in one line.
25
+
26
+ Plan: <plan-id> if applicable. Owner: <subsystem>.
27
+ """
28
+
29
+ import logging
30
+
31
+ from fastapi import APIRouter, Depends, HTTPException, Request
32
+ from pydantic import BaseModel, Field
33
+
34
+ # Adjust auth import to whatever your project uses (e.g., a shared deps module).
35
+ from ._shared import get_current_user # rename if your project differs
36
+ # from ..auth import require_role # for endpoints that mutate sensitive state
37
+
38
+ logger = logging.getLogger(__name__)
39
+
40
+ router = APIRouter(prefix="/api/<name>", tags=["<name>"])
41
+
42
+
43
+ class FooRequest(BaseModel):
44
+ symbol: str = Field(min_length=1, max_length=16)
45
+ # ALWAYS bound numeric inputs — open-ended floats are how unit-mismatch and
46
+ # APY-style overflow bugs sneak through.
47
+ quantity: float = Field(gt=0, le=1_000_000)
48
+
49
+
50
+ class FooResponse(BaseModel):
51
+ ok: bool
52
+ payload: dict
53
+
54
+
55
+ @router.get("/items")
56
+ async def list_items(
57
+ request: Request,
58
+ user: dict = Depends(get_current_user),
59
+ ) -> FooResponse:
60
+ """List items. Read-only — base auth dependency is sufficient."""
61
+ # Async-only I/O. Wrap external calls with `async with asyncio.timeout(N)`;
62
+ # client-library timeouts (httpx, aiohttp) alone do not cover DNS/TLS hangs.
63
+ return FooResponse(ok=True, payload={"items": []})
64
+
65
+
66
+ @router.post("/orders")
67
+ async def create_order(
68
+ body: FooRequest,
69
+ request: Request,
70
+ user: dict = Depends(get_current_user), # SWAP for require_role(...) for any state-mutating action
71
+ ) -> FooResponse:
72
+ """Mutating endpoint — enforce role-based auth (or service-token) here."""
73
+ logger.info("order created symbol=%s qty=%s user=%s", body.symbol, body.quantity, user.get("user_id"))
74
+ return FooResponse(ok=True, payload={"symbol": body.symbol, "qty": body.quantity})
75
+ ```
76
+
77
+ ## Registration in `main.py`
78
+
79
+ ```python
80
+ # at top of file with other router imports
81
+ from .routers.<name> import router as <name>_router
82
+
83
+ # in the section where other routers are included
84
+ app.include_router(<name>_router)
85
+ ```
86
+
87
+ ## Test scaffold (`${paths.python_test}/test_<name>_router.py`)
88
+
89
+ ```python
90
+ import pytest
91
+ from httpx import AsyncClient, ASGITransport
92
+
93
+ from <project_package>.main import app # substitute your top-level package
94
+
95
+
96
+ @pytest.mark.asyncio
97
+ async def test_list_items_requires_auth():
98
+ async with AsyncClient(transport=ASGITransport(app=app), base_url="http://test") as ac:
99
+ r = await ac.get("/api/<name>/items")
100
+ assert r.status_code in (401, 403)
101
+
102
+
103
+ @pytest.mark.asyncio
104
+ async def test_create_order_input_validation(auth_headers):
105
+ async with AsyncClient(transport=ASGITransport(app=app), base_url="http://test") as ac:
106
+ r = await ac.post("/api/<name>/orders", json={"symbol": "", "quantity": 0}, headers=auth_headers)
107
+ assert r.status_code == 422
108
+ ```
109
+
110
+ ## FastAPI Conventions (apply in any project)
111
+
112
+ - **Auth choice**:
113
+ - `Depends(get_current_user)` (or whatever your "is logged in" dep is) for read-only endpoints
114
+ - A role-gated dependency (e.g. `require_role("admin")`, `require_tier_or_guardian("trader")`, etc.) for ANY state-mutating endpoint — this is non-negotiable for safety-critical surfaces
115
+ - Service-token / machine-auth checks happen BEFORE any "auth disabled" dev bypass
116
+ - **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.
117
+ - **Bound numeric inputs**: `Field(gt=..., le=...)` on every numeric. Open-ended ranges produce overflow bugs in financial / metric / quantity contexts.
118
+ - **Validate input strings**: symbols, IDs, slugs — use a project-local validator, never trust raw `str` for downstream lookups.
119
+ - **No hardcoded sentinel values**: `0`, `0.00`, `""`, `None` are usually indistinguishable from real values. Be deliberate.
120
+ - **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.
121
+ - **Silent drops log WARNING** — never DEBUG. Anything dropped at scale must be visible in production logs.
122
+ - **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.
123
+
124
+ ## Process
125
+
126
+ 1. Ask user: what subsystem owns this router? What endpoints does it need? Which are read-only vs mutating?
127
+ 2. Confirm path: `${paths.python_source}/routers/<name>.py`. If the name collides with an existing router, stop and ask.
128
+ 3. Write the router file using the template above; pick the right auth dependency per endpoint.
129
+ 4. Write the test scaffold and confirm it imports cleanly: `pytest ${paths.python_test}/test_<name>_router.py -x --collect-only`.
130
+ 5. Add the `app.include_router(<name>_router)` line to `main.py` AFTER the file exists (split-commit safety).
131
+ 6. Verify route registration:
132
+ ```bash
133
+ python -c "from <project_package>.main import app; print([r.path for r in app.routes if '/api/<name>' in str(r.path)])"
134
+ ```
135
+ 7. Restart the service and curl-smoke the new endpoint (tests passing ≠ running process has the change). Use whatever process manager your project declares — `launchctl kickstart`, `systemctl restart`, `pm2 restart`, `docker compose up -d`, etc.
136
+
137
+ ## START NOW
138
+
139
+ Ask the user:
140
+ 1. What subsystem/feature owns this router?
141
+ 2. What's the URL prefix? (default: `/api/<name>`)
142
+ 3. What endpoints, and which are mutating vs read-only?
143
+ 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.