@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 ADDED
@@ -0,0 +1,40 @@
1
+ # @massu/core
2
+
3
+ AI Engineering Governance MCP Server — session memory, feature registry, code intelligence, and rule enforcement for AI coding assistants.
4
+
5
+ ## Quick Start
6
+
7
+ ```bash
8
+ npx massu init
9
+ ```
10
+
11
+ This sets up the MCP server, configuration, and lifecycle hooks in one command.
12
+
13
+ ## What is Massu?
14
+
15
+ Massu is a source-available [Model Context Protocol (MCP)](https://modelcontextprotocol.io/) server that adds governance capabilities to AI coding assistants like Claude Code. It provides:
16
+
17
+ - **73 MCP Tools** — quality analytics, cost tracking, security scoring, dependency analysis, and more
18
+ - **15 Lifecycle Hooks** — pre-commit gates, security scanning, intent suggestion, session management, and auto-learning pipeline
19
+ - **3-Database Architecture** — code graph (read-only), data (imports/mappings), and memory (sessions/analytics)
20
+ - **Config-Driven** — all project-specific data lives in `massu.config.yaml`
21
+
22
+ ## Usage
23
+
24
+ After `npx massu init`, your AI assistant gains access to all governance tools automatically via the MCP protocol.
25
+
26
+ ```bash
27
+ # Health check
28
+ npx massu doctor
29
+
30
+ # Validate configuration
31
+ npx massu validate-config
32
+ ```
33
+
34
+ ## Documentation
35
+
36
+ Full documentation at [massu.ai](https://massu.ai).
37
+
38
+ ## License
39
+
40
+ [BSL 1.1](https://github.com/massu-ai/massu/blob/main/LICENSE) — source-available. Free to use, modify, and distribute. See LICENSE for full terms.
@@ -0,0 +1,122 @@
1
+ # Massu Slash-Command Templates
2
+
3
+ This directory ships the slash commands installed into a consumer project's `<claudeDirName>/commands/` (default `.claude/commands/`) when the consumer runs `npx @massu/core install-commands` (or `npx @massu/core init`, which calls install-commands as part of its flow).
4
+
5
+ As of `@massu/core@1.3.0`, command files are **stack-aware**: a template can ship one or more language-specific variants alongside the default, and the installer picks the variant that matches the consumer's `massu.config.yaml`. Local edits are preserved across reinstalls via a manifest written to `<claudeDirName>/.massu/install-manifest.json`.
6
+
7
+ This README covers:
8
+
9
+ 1. The variant filename convention.
10
+ 2. The variant-resolution algorithm (priority order + tie-break).
11
+ 3. The local-edit-protection manifest.
12
+ 4. The `npx @massu/core show-template <name>` debugging command.
13
+
14
+ ## 1. Variant filename convention
15
+
16
+ ```
17
+ <base>.md # default — used when no variant matches
18
+ <base>.python.md # FastAPI / Django / generic Python
19
+ <base>.swift.md # SwiftUI / iOS / visionOS
20
+ <base>.rust.md # axum / actix / generic Rust
21
+ <base>.typescript.md # reserved for future use; currently no variants ship
22
+ ```
23
+
24
+ The variant convention applies **only at the top level** of `packages/core/commands/`. Files inside subdirectories (e.g., `_shared-references/`, `massu-loop/references/`) are copied recursively as-is — no variant resolution, no dot-skip filtering. Future authors can use dotted filenames in nested dirs without losing them to the variant filter.
25
+
26
+ The variant convention is **opt-in**: a template that does NOT ship any `<base>.<variant>.md` siblings is variant-agnostic and the unsuffixed `<base>.md` is used everywhere.
27
+
28
+ ## 2. Variant-resolution algorithm
29
+
30
+ Given a base template name `B` and the consumer's `framework` config from `massu.config.yaml`:
31
+
32
+ 1. **Build the candidate list `V`** in priority order:
33
+ 1. `framework.primary` (or `framework.type` if `primary` is undefined). Skipped if the value is `"multi"`.
34
+ 2. Each declared `framework.languages.<lang>` entry whose `framework` field is a non-empty string, in **YAML declaration order** (first-declared wins on ties).
35
+ 3. **Passthrough fallback**: well-known top-level `framework.<lang>` blocks (typescript / javascript / python / swift / rust / go) with a non-empty `framework` field, in fixed order, excluding any language already covered. This is what lets a project that declares `framework.swift` at the top level (alongside `framework.languages.python`) still pick up the `.swift.md` variant when the languages block doesn't contain swift.
36
+ 4. The unsuffixed default (`""`) as the last fallback.
37
+ 2. **Probe disk** in order: for each candidate `c`, check whether `<base>.<c>.md` exists in the bundled commands directory.
38
+ 3. **Return**:
39
+ - First hit → copy that file.
40
+ - No hit AND `framework.type === "multi"` AND `framework.primary` is undefined → write a one-line warning to stderr and copy the unsuffixed default.
41
+ - No hit otherwise → skip the file (this only happens if the base template was deleted and only orphan variants remain, which the Phase 0 audit prevents).
42
+
43
+ The target filename in the consumer dir is always the BASE name (`<base>.md`) — variant suffixes are an internal package detail.
44
+
45
+ ### Tie-break — which variant wins?
46
+
47
+ If a consumer declares both `python` and `swift` in `framework.languages` AND a template ships both `.python.md` and `.swift.md`, the variant declared FIRST in `framework.languages` wins. Consumers control this by reordering keys in `massu.config.yaml`. Passthrough-fallback entries (rule 1.3) are appended AFTER all `framework.languages` entries.
48
+
49
+ There is no per-template override mechanism. If you need one, file an issue.
50
+
51
+ ## 3. Local-edit protection — the manifest
52
+
53
+ `@massu/core@1.3.0` introduces a manifest at:
54
+
55
+ ```
56
+ <consumer-root>/<claudeDirName>/.massu/install-manifest.json
57
+ ```
58
+
59
+ (where `claudeDirName` is the value of `conventions.claudeDirName` in `massu.config.yaml`, defaulting to `.claude`).
60
+
61
+ Each entry maps an asset-relative path (e.g., `commands/massu-scaffold-router.md`) to the SHA-256 of its content at the last successful install.
62
+
63
+ ### What the installer does on each run
64
+
65
+ For each file `<asset>` to be installed, three hashes are computed:
66
+
67
+ | Hash | What it represents |
68
+ |------|--------------------|
69
+ | `sourceHash` | The bundled upstream content NOW. |
70
+ | `existingHash` | The current consumer file content. `undefined` if missing. |
71
+ | `lastInstalledHash` | The hash recorded in the manifest. `undefined` on first install. |
72
+
73
+ Decision matrix:
74
+
75
+ | Condition | Action | Counter |
76
+ |-----------|--------|---------|
77
+ | target missing | write upstream, record `sourceHash` in manifest | `installed` |
78
+ | `existingHash === sourceHash` | already in sync; record `sourceHash` (covers manifest healing) | `skipped` |
79
+ | `lastInstalledHash` undefined AND `existingHash !== sourceHash` | first-install heuristic: keep the consumer file, record `existingHash`, print a one-line notice | `kept` |
80
+ | `existingHash !== lastInstalledHash` (consumer edited it) | preserve, print `kept your version` notice + diff hint | `kept` |
81
+ | `existingHash === lastInstalledHash` AND `sourceHash !== lastInstalledHash` (clean upstream upgrade) | write upstream, record `sourceHash` | `updated` |
82
+
83
+ The manifest is written **atomically** (`tempfile + renameSync`) so a crash mid-install never leaves a partially-written manifest.
84
+
85
+ ### What this means for you
86
+
87
+ - Edit your `<claudeDirName>/commands/*.md` freely. The installer will not stomp your edits.
88
+ - To accept the upstream version of a file you've edited: delete it and rerun `install-commands`.
89
+ - To diff your version against upstream:
90
+ ```bash
91
+ diff .claude/commands/massu-scaffold-router.md \
92
+ <(npx @massu/core show-template massu-scaffold-router)
93
+ ```
94
+
95
+ ## 4. `npx @massu/core show-template <name>`
96
+
97
+ Prints the resolved template content (post-variant-resolution) to stdout. Used in the diff one-liner above. Accepts both `massu-scaffold-router` and `massu-scaffold-router.md`. Exits 1 on unknown names.
98
+
99
+ ## 5. Currently shipped variants
100
+
101
+ | Base | Variants |
102
+ |------|----------|
103
+ | `massu-scaffold-router` | `.python.md` (FastAPI) |
104
+ | `massu-scaffold-page` | `.swift.md` (SwiftUI), regenerated default with embedded Next.js / FastAPI / SwiftUI / Rust examples |
105
+ | `massu-deploy` | `.python.md` (launchd / systemd / pm2 / docker) |
106
+
107
+ All other 57 top-level templates ship as variant-agnostic defaults (one `<base>.md`).
108
+
109
+ ## 6. Adding a new variant
110
+
111
+ 1. Create `<base>.<lang>.md` in this directory using the same frontmatter shape as the default.
112
+ 2. The default `<base>.md` should remain generic (or be regenerated if it was previously stack-specific — see `massu-scaffold-page.md` for the pattern of an embedded multi-stack default).
113
+ 3. Add a row to the table in section 5.
114
+ 4. Add a test case to `packages/core/src/__tests__/install-commands.test.ts` that exercises the new variant against a fixture config.
115
+ 5. Update `docs/internal/2026-04-26-template-variant-audit.md` (the audit doc) with the new label.
116
+
117
+ ## 7. Reference
118
+
119
+ - Plan: `/Users/ekoultra/hedge/docs/plans/2026-04-26-massu-stack-aware-command-templates.md`
120
+ - Audit: `docs/internal/2026-04-26-template-variant-audit.md`
121
+ - Implementation: `packages/core/src/commands/install-commands.ts`
122
+ - Tests: `packages/core/src/__tests__/install-commands.test.ts` and `show-template.test.ts`
@@ -0,0 +1,200 @@
1
+ ---
2
+ name: massu-deploy
3
+ description: "Deploy a Python service (FastAPI / asgi) to production. Default target is a long-running process supervised by launchd, systemd, pm2, or docker — NOT Vercel. Asks which surface before acting."
4
+ allowed-tools: Bash(*), Read(*), Grep(*), Glob(*)
5
+ ---
6
+
7
+ > **Shared rules apply.** Read `${paths.commands}/_shared-preamble.md` before proceeding.
8
+
9
+ # Massu Deploy: Python Service Production Deploy
10
+
11
+ ## Workflow Position
12
+
13
+ ```
14
+ /massu-create-plan -> /massu-plan -> /massu-loop -> /massu-commit -> /massu-push -> /massu-deploy
15
+ (CREATE) (AUDIT) (IMPLEMENT) (COMMIT) (PUSH) (DEPLOY)
16
+ ```
17
+
18
+ This command deploys to production. **If the service is doing anything financial, transactional, or otherwise consequential, real money / data integrity is at risk** — pre-flight checks are mandatory.
19
+
20
+ ---
21
+
22
+ ## CRITICAL — Deployment Surfaces
23
+
24
+ A non-Vercel Python project usually has multiple deploy surfaces. Ask the user which one(s) before proceeding:
25
+
26
+ | Surface | What "deploy" means | Authority |
27
+ |---------|---------------------|-----------|
28
+ | **Service** (default) | Restart the supervised process — `launchctl kickstart`, `systemctl restart`, `pm2 restart`, or `docker compose up -d` | The actual production brain |
29
+ | **Static / docs** | rsync / S3 sync (if applicable) | Static site only |
30
+ | **Worker(s)** | Restart any background workers (celery, rq, custom asyncio task runners) | Background processing |
31
+
32
+ **There is NO assumption of Vercel.** Do NOT run `vercel --prod` from this template. If the user wants a Vercel deploy of a separate sub-project, route them to a different surface or scaffold a dedicated script.
33
+
34
+ ---
35
+
36
+ ## NON-NEGOTIABLE RULES
37
+
38
+ - **Never deploy with uncommitted changes** — push first via `/massu-push`
39
+ - **Never deploy with failing tests** — `pytest` (or your project's test runner) must be green
40
+ - **Always restart-and-probe** — file-saved ≠ process-running-the-fix. After restart, hit a health endpoint and diff before/after
41
+ - **Never kill processes without identifying them first** — get the PID, log line, or supervisor label before sending any signal
42
+ - **Live-trading or live-billing flag flips are a separate concern** — this command does not flip `auto_*_mode`, `*_live`, `production_*` flags. Use the dedicated approval flow
43
+
44
+ ---
45
+
46
+ ## START NOW
47
+
48
+ ### Step 0: Ask the user
49
+
50
+ ```
51
+ ===============================================================================
52
+ PYTHON DEPLOY — Which surface?
53
+ ===============================================================================
54
+
55
+ service Restart the supervised Python service (default)
56
+ workers Restart background workers (celery / rq / custom)
57
+ all service + workers
58
+ static rsync / S3 sync (only if applicable)
59
+
60
+ Which? [service / workers / all / static]
61
+ ===============================================================================
62
+ ```
63
+
64
+ ---
65
+
66
+ ## Path A: Service (default)
67
+
68
+ Substitute `${supervisor}` with whatever your project actually uses (`launchd`, `systemd`, `pm2`, `docker`). Substitute `${service_label}` with the actual label / unit name from `massu.config.yaml` (`deploy.python.service_label`) or from your project's process manager.
69
+
70
+ ### Pre-flight
71
+
72
+ ```bash
73
+ # 1. Branch + working tree clean
74
+ test -z "$(git status --porcelain)" || { echo "DIRTY — commit/stash first"; exit 1; }
75
+
76
+ # 2. Tests green (impacted area only — full suite is for /massu-push)
77
+ ${paths.python_test_command:-pytest} -x 2>&1 | tail -10
78
+
79
+ # 3. Identify the running process (NEVER blind-kill)
80
+ # launchd: launchctl list | grep ${service_label}
81
+ # systemd: systemctl status ${service_label}
82
+ # pm2: pm2 jlist | jq '.[] | select(.name=="${service_label}")'
83
+ # docker: docker compose ps ${service_label}
84
+
85
+ # 4. Capture current health for diff
86
+ curl -sS ${health_url:-http://localhost:8000/health} | python3 -m json.tool > /tmp/${service_label}-health-before.json
87
+ ```
88
+
89
+ ### Approval gate
90
+
91
+ ```
92
+ ===============================================================================
93
+ APPROVAL REQUIRED — RESTART ${service_label}
94
+ ===============================================================================
95
+
96
+ Pre-flight: PASS
97
+ Branch: <current>
98
+ HEAD: <sha>
99
+ Last service uptime: <etime>
100
+
101
+ This will:
102
+ 1. Restart ${service_label} via ${supervisor}
103
+ 2. Wait 5s, poll /health until 200 (max 60s)
104
+ 3. Smoke: /health, /api/feature-flags (if applicable), one critical endpoint
105
+ 4. Diff before/after health responses
106
+ 5. If any check fails: print rollback (`git revert HEAD` + restart)
107
+
108
+ Reply "approve" or "abort".
109
+ ===============================================================================
110
+ ```
111
+
112
+ ### Restart + verify
113
+
114
+ Pick the line that matches your supervisor:
115
+
116
+ ```bash
117
+ # launchd
118
+ launchctl kickstart -k gui/$(id -u)/${service_label}
119
+
120
+ # systemd (user)
121
+ systemctl --user restart ${service_label}
122
+
123
+ # pm2
124
+ pm2 restart ${service_label}
125
+
126
+ # docker compose
127
+ docker compose up -d --force-recreate ${service_label}
128
+ ```
129
+
130
+ Then probe:
131
+
132
+ ```bash
133
+ # Poll until ready (60s budget)
134
+ for i in $(seq 1 30); do
135
+ sleep 2
136
+ status=$(curl -sS -o /dev/null -w "%{http_code}" ${health_url:-http://localhost:8000/health} || echo "000")
137
+ [ "$status" = "200" ] && { echo "READY after ${i} polls"; break; }
138
+ done
139
+
140
+ # Smoke + diff
141
+ curl -sS ${health_url:-http://localhost:8000/health} | python3 -m json.tool > /tmp/${service_label}-health-after.json
142
+ diff /tmp/${service_label}-health-before.json /tmp/${service_label}-health-after.json | head -50
143
+
144
+ # Tail the supervisor log for startup errors
145
+ # launchd:
146
+ log show --predicate 'subsystem == "${service_label}"' --last 2m --info 2>/dev/null | grep -iE "error|warn" | head -20
147
+ # systemd:
148
+ # journalctl --user -u ${service_label} --since "2 minutes ago" | grep -iE "error|warn" | head -20
149
+ # pm2:
150
+ # pm2 logs ${service_label} --lines 100 --nostream | grep -iE "error|warn" | head -20
151
+ ```
152
+
153
+ ### Rollback
154
+
155
+ If `/health` does not return 200 within 60s, or smoke fails:
156
+
157
+ ```bash
158
+ git revert HEAD --no-edit && <restart-command-from-above>
159
+ ```
160
+
161
+ Then page yourself or your on-call: an unhealthy production service is an incident.
162
+
163
+ ---
164
+
165
+ ## Path B: Workers
166
+
167
+ ```bash
168
+ # Restart background workers (celery / rq / custom asyncio runners)
169
+ # Substitute the supervisor label list for your project's worker names.
170
+ for label in ${worker_labels}; do
171
+ echo "restarting $label..."
172
+ # launchd: launchctl kickstart -k gui/$(id -u)/$label
173
+ # systemd: systemctl --user restart $label
174
+ # pm2: pm2 restart $label
175
+ done
176
+
177
+ # Verify each worker is processing again — push a no-op job if you have one,
178
+ # or read the queue depth before/after.
179
+ ```
180
+
181
+ ---
182
+
183
+ ## Path C: Static (only if applicable)
184
+
185
+ ```bash
186
+ # Build static site, rsync, or aws s3 sync — fill in your project's pipeline.
187
+ # This is intentionally NOT a default; only run if your project has a static deploy target.
188
+ ```
189
+
190
+ ---
191
+
192
+ ## After ANY surface deploys
193
+
194
+ Save the deployment sha and timestamp to your project's audit log so the boundary is visible:
195
+
196
+ ```bash
197
+ echo "$(date -u +%FT%TZ) deploy surface=<surface> sha=$(git rev-parse HEAD) actor=$(whoami)" >> ${paths.audit_log:-data/audit/deploys.log}
198
+ ```
199
+
200
+ Done. Report: surface, sha, time, smoke results, any warnings.
@@ -1,24 +1,40 @@
1
1
  ---
2
2
  name: massu-scaffold-page
3
- description: "When user wants to create a new page, route, or section in the app scaffolds the page component, layout, loading/error states, and tRPC integration"
3
+ description: "When user wants to create a new page, screen, or view asks which framework target (Next.js, SwiftUI, FastAPI templates, Rust web), then scaffolds component / view / handler with project conventions"
4
4
  allowed-tools: Bash(*), Read(*), Write(*), Edit(*), Grep(*), Glob(*)
5
5
  ---
6
6
 
7
- # Scaffold New Page
7
+ # Scaffold New Page / View
8
8
 
9
- Creates a complete Next.js App Router page following all project patterns.
9
+ This default template is **framework-agnostic**. It asks the user (or, when invoked by an agent, infers from `massu.config.yaml`) which target to scaffold, then dispatches to one of the embedded patterns below.
10
10
 
11
- ## What Gets Created
11
+ > **If your project ships a stack-specific variant** of this template (e.g., `massu-scaffold-page.swift.md`), the variant is preferred and this default is not installed. See `${paths.commands}/README.md` for the variant-resolution rules.
12
12
 
13
- | File | Purpose |
14
- |------|---------|
15
- | `page.tsx` | Main page component with Suspense boundary |
16
- | `loading.tsx` | Skeleton loading state |
17
- | `error.tsx` | Error boundary with retry |
13
+ ## Step 1 Pick a target
18
14
 
19
- ## Template
15
+ Ask the user (or read `framework.primary` / `framework.languages` from the consumer's `massu.config.yaml`):
16
+
17
+ | Stack | Path A: web | Path B: native / mobile | Path C: backend rendered |
18
+ |-------|-------------|-------------------------|--------------------------|
19
+ | TypeScript / Next.js | `app/<route>/page.tsx` + loading + error | — | — |
20
+ | Swift / SwiftUI | — | `Features/<feature>/Views/<Name>View.swift` (+ ViewModel + Response) | — |
21
+ | Python / FastAPI | — | — | Jinja template + handler in `routers/<name>.py` |
22
+ | Rust / Axum | — | — | `src/handlers/<name>.rs` returning `Html` or JSON |
23
+
24
+ If the user is unsure, default to whatever `framework.primary` points at.
25
+
26
+ ---
27
+
28
+ ## Pattern 1 — Next.js App Router page
29
+
30
+ ### What gets created
31
+
32
+ - `${paths.web_source}/app/<route>/page.tsx`
33
+ - `${paths.web_source}/app/<route>/loading.tsx`
34
+ - `${paths.web_source}/app/<route>/error.tsx`
35
+
36
+ ### `page.tsx`
20
37
 
21
- ### page.tsx
22
38
  ```tsx
23
39
  import { Suspense } from 'react';
24
40
  import { PageContent } from './page-content';
@@ -26,82 +42,179 @@ import Loading from './loading';
26
42
 
27
43
  export default function Page() {
28
44
  return (
29
- <div className="page-container">
30
- <Suspense fallback={<Loading />}>
31
- <PageContent />
32
- </Suspense>
33
- </div>
45
+ <Suspense fallback={<Loading />}>
46
+ <PageContent />
47
+ </Suspense>
34
48
  );
35
49
  }
36
50
  ```
37
51
 
38
- ### page-content.tsx (with params)
52
+ ### `page-content.tsx`
53
+
39
54
  ```tsx
40
55
  'use client';
41
56
 
42
- import { use } from 'react';
43
- import { api } from '@/trpc/react';
44
-
45
- export function PageContent({ params }: { params: Promise<{ id: string }> }) {
46
- const { id } = use(params);
47
- const { data, isLoading } = api.routerName.getById.useQuery({ id });
48
-
49
- if (isLoading) return <Loading />;
50
-
51
- return (
52
- <>
53
- <PageHeader title="Page Title" />
54
- {/* Page content */}
55
- </>
56
- );
57
+ import { useEffect, useState } from 'react';
58
+
59
+ export function PageContent() {
60
+ const [data, setData] = useState<unknown>(null);
61
+ const [error, setError] = useState<string | null>(null);
62
+
63
+ useEffect(() => {
64
+ // Substitute the URL with your project's API base — tRPC client, REST URL, etc.
65
+ fetch(`${process.env.NEXT_PUBLIC_API_BASE ?? '/api'}/<endpoint>`, {
66
+ credentials: 'include',
67
+ })
68
+ .then(async (r) => {
69
+ if (!r.ok) throw new Error(`HTTP ${r.status}`);
70
+ setData(await r.json());
71
+ })
72
+ .catch((e: Error) => setError(e.message));
73
+ }, []);
74
+
75
+ if (error) throw new Error(error);
76
+ if (!data) return null;
77
+ return <pre>{JSON.stringify(data, null, 2)}</pre>;
57
78
  }
58
79
  ```
59
80
 
60
- ### loading.tsx
61
- ```tsx
62
- import { Skeleton } from '@/components/ui/skeleton';
81
+ ### `loading.tsx` / `error.tsx`
63
82
 
83
+ ```tsx
84
+ // loading.tsx — show a skeleton, NOT a flash of empty state.
64
85
  export default function Loading() {
65
- return (
66
- <div className="page-container">
67
- <Skeleton className="h-8 w-48 mb-6" />
68
- <Skeleton className="h-64 w-full" />
69
- </div>
70
- );
86
+ return <div className="page-skeleton" />;
71
87
  }
72
88
  ```
73
89
 
74
- ### error.tsx
75
90
  ```tsx
91
+ // error.tsx — must be a client component. Provides a retry handle to the user.
76
92
  'use client';
77
-
78
93
  export default function Error({ error, reset }: { error: Error; reset: () => void }) {
79
94
  return (
80
- <div className="page-container">
81
- <h2>Something went wrong</h2>
82
- <p className="text-muted-foreground">{error.message}</p>
95
+ <div role="alert">
96
+ <p>Something went wrong: {error.message}</p>
83
97
  <button onClick={reset}>Try again</button>
84
98
  </div>
85
99
  );
86
100
  }
87
101
  ```
88
102
 
89
- ## Gotchas
103
+ ### Verification
104
+
105
+ ```bash
106
+ npm run build 2>&1 | tail -20 # or pnpm/yarn equivalent
107
+ ```
108
+
109
+ ---
110
+
111
+ ## Pattern 2 — SwiftUI iOS / visionOS view
112
+
113
+ (Full version available as the `.swift.md` variant of this template.)
114
+
115
+ ### What gets created
116
+
117
+ - `${paths.swift_source}/Features/<feature>/Views/<Name>View.swift`
118
+ - `${paths.swift_source}/Features/<feature>/ViewModels/<Name>ViewModel.swift`
119
+ - `${paths.swift_source}/Features/<feature>/Models/<Name>Response.swift`
120
+
121
+ ### Sketch
122
+
123
+ ```swift
124
+ struct <Name>View: View {
125
+ @StateObject private var viewModel = <Name>ViewModel()
126
+ var body: some View {
127
+ Group {
128
+ if viewModel.isLoading { ProgressView() }
129
+ else if let err = viewModel.error { ErrorState(message: err) { Task { await viewModel.load() } } }
130
+ else { content }
131
+ }
132
+ .task { await viewModel.load() }
133
+ }
134
+ private var content: some View { /* real content */ EmptyView() }
135
+ }
136
+
137
+ @MainActor
138
+ final class <Name>ViewModel: ObservableObject {
139
+ @Published var data: <Name>Response?
140
+ @Published var isLoading = false
141
+ @Published var error: String?
142
+ func load() async { /* fetch and decode */ }
143
+ }
144
+ ```
145
+
146
+ **Critical**: With `JSONDecoder.keyDecodingStrategy = .convertFromSnakeCase`, mismatched property names decode to nil silently. Hand-verify every Decodable property against a real API response.
147
+
148
+ ### Verification
149
+
150
+ ```bash
151
+ xcodebuild -scheme <Target>_iOS -destination 'generic/platform=iOS Simulator' build | tail -20
152
+ ```
153
+
154
+ ---
155
+
156
+ ## Pattern 3 — FastAPI Jinja-rendered page
90
157
 
91
- - **ALWAYS use `page-container`** class on root div — never `container mx-auto`
92
- - **Suspense boundaries** are REQUIRED for pages using `use(params)` or `useSearchParams()`
93
- - **protectedProcedure** for ALL tRPC queries that need auth
94
- - **No `sm:page-container`** — only exception is mobile chat layouts
95
- - **Breadcrumbs**: Add to PageHeader if page is nested (e.g., `/crm/contacts/[id]`)
158
+ ### What gets created
96
159
 
97
- ## Process
160
+ - `${paths.python_source}/routers/<name>.py` (Jinja-rendering handler)
161
+ - `${paths.python_templates}/<name>.html`
98
162
 
99
- 1. Ask user: What URL path? What data does it fetch?
100
- 2. Determine route segment: `src/app/(app)/[section]/[subsection]/`
101
- 3. Create all 3 files (page.tsx, loading.tsx, error.tsx)
102
- 4. If data-fetching: verify tRPC router exists, add query
103
- 5. Run `npx tsc --noEmit` to verify types
163
+ ### Sketch
164
+
165
+ ```python
166
+ from fastapi import APIRouter, Depends, Request
167
+ from fastapi.responses import HTMLResponse
168
+ from fastapi.templating import Jinja2Templates
169
+
170
+ from ._shared import get_current_user
171
+
172
+ router = APIRouter()
173
+ templates = Jinja2Templates(directory="${paths.python_templates}")
174
+
175
+ @router.get("/<route>", response_class=HTMLResponse)
176
+ async def render(request: Request, user: dict = Depends(get_current_user)):
177
+ return templates.TemplateResponse("<name>.html", {"request": request, "user": user})
178
+ ```
179
+
180
+ **Critical**: this pattern serves HTML from FastAPI directly; for pure-API endpoints, use `/massu-scaffold-router` instead.
181
+
182
+ ---
183
+
184
+ ## Pattern 4 — Axum (Rust) HTML handler
185
+
186
+ ### What gets created
187
+
188
+ - `${paths.rust_source}/handlers/<name>.rs`
189
+ - Wire-up in `src/main.rs` or `src/router.rs` via `.route("/<path>", get(<name>::handler))`.
190
+
191
+ ### Sketch
192
+
193
+ ```rust
194
+ use axum::{response::Html, routing::get, Router};
195
+
196
+ pub fn router() -> Router {
197
+ Router::new().route("/<path>", get(handler))
198
+ }
199
+
200
+ async fn handler() -> Html<&'static str> {
201
+ Html("<!doctype html><meta charset='utf-8'><title>Page</title><h1>Hello</h1>")
202
+ }
203
+ ```
204
+
205
+ ### Verification
206
+
207
+ ```bash
208
+ cargo build 2>&1 | tail -10 && cargo test --quiet
209
+ ```
210
+
211
+ ---
104
212
 
105
213
  ## START NOW
106
214
 
107
- Ask the user what page to create and where it should live in the route hierarchy.
215
+ Ask the user (in this order):
216
+
217
+ 1. **Which target?** TS / Next.js · Swift / SwiftUI · Python / FastAPI · Rust / Axum. Default to `framework.primary` from `massu.config.yaml` if the user is unsure.
218
+ 2. What's the URL path / feature name?
219
+ 3. What does the page render, and which API endpoint feeds it?
220
+ 4. Any auth requirements? (logged-in only · role-gated · biometric-gated for sensitive actions)