@massu/core 1.2.1 → 1.3.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.
@@ -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}/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 |
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.).
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 APIClient with the project's actual API wrapper.
62
+ private let api: APIClient
63
+
64
+ init(api: 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.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}/.. && xcodegen`.
111
+ 5. Build the right scheme:
112
+ ```bash
113
+ cd ${paths.swift_source}/.. && 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,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.