@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.
- package/README.md +40 -0
- package/commands/README.md +122 -0
- package/commands/massu-deploy.python.md +200 -0
- package/commands/massu-scaffold-page.md +172 -59
- package/commands/massu-scaffold-page.swift.md +121 -0
- package/commands/massu-scaffold-router.python.md +143 -0
- package/dist/cli.js +482 -223
- package/dist/hooks/auto-learning-pipeline.js +7 -4
- package/dist/hooks/classify-failure.js +7 -4
- package/dist/hooks/cost-tracker.js +7 -4
- package/dist/hooks/fix-detector.js +7 -4
- package/dist/hooks/incident-pipeline.js +7 -4
- package/dist/hooks/post-edit-context.js +7 -4
- package/dist/hooks/post-tool-use.js +7 -4
- package/dist/hooks/pre-compact.js +7 -4
- package/dist/hooks/pre-delete-check.js +7 -4
- package/dist/hooks/quality-event.js +7 -4
- package/dist/hooks/rule-enforcement-pipeline.js +7 -4
- package/dist/hooks/session-end.js +7 -4
- package/dist/hooks/session-start.js +7 -4
- package/dist/hooks/user-prompt.js +7 -4
- package/package.json +1 -1
- package/src/cli.ts +6 -0
- package/src/commands/install-commands.ts +366 -42
- package/src/commands/show-template.ts +65 -0
- package/src/config.ts +6 -3
|
@@ -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.
|