mvc-kit 2.0.0 → 2.2.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 +15 -0
- package/agent-config/bin/postinstall.mjs +41 -0
- package/agent-config/bin/setup.mjs +216 -0
- package/agent-config/claude-code/.claude-plugin/plugin.json +5 -0
- package/agent-config/claude-code/agents/mvc-kit-architect.md +97 -0
- package/agent-config/claude-code/skills/guide/SKILL.md +85 -0
- package/agent-config/claude-code/skills/guide/anti-patterns.md +321 -0
- package/agent-config/claude-code/skills/guide/api-reference.md +310 -0
- package/agent-config/claude-code/skills/guide/patterns.md +336 -0
- package/agent-config/claude-code/skills/review/SKILL.md +53 -0
- package/agent-config/claude-code/skills/review/checklist.md +89 -0
- package/agent-config/claude-code/skills/scaffold/SKILL.md +52 -0
- package/agent-config/claude-code/skills/scaffold/templates/channel.md +88 -0
- package/agent-config/claude-code/skills/scaffold/templates/collection.md +49 -0
- package/agent-config/claude-code/skills/scaffold/templates/controller.md +56 -0
- package/agent-config/claude-code/skills/scaffold/templates/eventbus.md +54 -0
- package/agent-config/claude-code/skills/scaffold/templates/model.md +102 -0
- package/agent-config/claude-code/skills/scaffold/templates/page-component.md +58 -0
- package/agent-config/claude-code/skills/scaffold/templates/service.md +101 -0
- package/agent-config/claude-code/skills/scaffold/templates/viewmodel.md +128 -0
- package/agent-config/copilot/copilot-instructions.md +242 -0
- package/agent-config/cursor/cursorrules +242 -0
- package/agent-config/lib/install-claude.mjs +87 -0
- package/package.json +7 -2
package/README.md
CHANGED
|
@@ -15,6 +15,21 @@ Zero-
|
|
|
15
15
|
npm install mvc-kit
|
|
16
16
|
```
|
|
17
17
|
|
|
18
|
+
## AI Agent Plugin
|
|
19
|
+
|
|
20
|
+
mvc-kit ships with built-in context for AI coding assistants — stays in sync with your installed version automatically.
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
npx mvc-kit-setup # Set up all agents
|
|
24
|
+
npx mvc-kit-setup claude # Claude Code only
|
|
25
|
+
npx mvc-kit-setup cursor # Cursor only
|
|
26
|
+
npx mvc-kit-setup copilot # GitHub Copilot only
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
**Claude Code** — installs `.claude/rules/` and `.claude/commands/`. Auto-updates on subsequent `npm install`/`npm update`.
|
|
30
|
+
|
|
31
|
+
**Cursor** — writes `.cursorrules`. **Copilot** — writes `.github/copilot-instructions.md`. Both use idempotent markers, safe to re-run.
|
|
32
|
+
|
|
18
33
|
## Quick Start
|
|
19
34
|
|
|
20
35
|
```typescript
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { existsSync } from 'node:fs';
|
|
4
|
+
import { resolve, join, dirname } from 'node:path';
|
|
5
|
+
import { fileURLToPath } from 'node:url';
|
|
6
|
+
|
|
7
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
8
|
+
const __dirname = dirname(__filename);
|
|
9
|
+
|
|
10
|
+
// INIT_CWD is set by npm/yarn/pnpm during lifecycle scripts — points to the project root
|
|
11
|
+
const projectRoot = process.env.INIT_CWD;
|
|
12
|
+
|
|
13
|
+
if (!projectRoot) {
|
|
14
|
+
process.exit(0);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
// Detect self-install (developing mvc-kit itself) — skip
|
|
18
|
+
const ownPackageRoot = resolve(__dirname, '../..');
|
|
19
|
+
if (resolve(projectRoot) === ownPackageRoot) {
|
|
20
|
+
process.exit(0);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// If the developer previously opted in (files exist), auto-update them
|
|
24
|
+
const rulesFile = join(projectRoot, '.claude', 'rules', 'mvc-kit.md');
|
|
25
|
+
|
|
26
|
+
if (existsSync(rulesFile)) {
|
|
27
|
+
try {
|
|
28
|
+
const { installClaude } = await import('../lib/install-claude.mjs');
|
|
29
|
+
installClaude(projectRoot);
|
|
30
|
+
console.log('');
|
|
31
|
+
console.log(' mvc-kit — Claude Code rules updated');
|
|
32
|
+
console.log('');
|
|
33
|
+
} catch {
|
|
34
|
+
// Never break npm install
|
|
35
|
+
}
|
|
36
|
+
} else {
|
|
37
|
+
console.log('');
|
|
38
|
+
console.log(' mvc-kit — AI agent setup available');
|
|
39
|
+
console.log(' Run: npx mvc-kit-setup');
|
|
40
|
+
console.log('');
|
|
41
|
+
}
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { readFileSync, writeFileSync, mkdirSync, existsSync } from 'node:fs';
|
|
4
|
+
import { join, dirname, resolve } from 'node:path';
|
|
5
|
+
import { fileURLToPath } from 'node:url';
|
|
6
|
+
import { createInterface } from 'node:readline';
|
|
7
|
+
import { installClaude } from '../lib/install-claude.mjs';
|
|
8
|
+
|
|
9
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
10
|
+
const __dirname = dirname(__filename);
|
|
11
|
+
const AGENT_CONFIG_DIR = resolve(__dirname, '..');
|
|
12
|
+
const PROJECT_DIR = process.cwd();
|
|
13
|
+
|
|
14
|
+
const MARKER_BEGIN = '<!-- BEGIN mvc-kit -->';
|
|
15
|
+
const MARKER_END = '<!-- END mvc-kit -->';
|
|
16
|
+
|
|
17
|
+
const VALID_TARGETS = ['claude', 'cursor', 'copilot', 'all'];
|
|
18
|
+
|
|
19
|
+
// ─── Helpers ──────────────────────────────────────────────
|
|
20
|
+
|
|
21
|
+
function printUsage() {
|
|
22
|
+
console.log(`
|
|
23
|
+
Usage: npx mvc-kit-setup [targets...] [options]
|
|
24
|
+
|
|
25
|
+
Targets:
|
|
26
|
+
claude Install Claude Code rules and commands into .claude/
|
|
27
|
+
cursor Copy mvc-kit rules to .cursorrules
|
|
28
|
+
copilot Copy mvc-kit instructions to .github/copilot-instructions.md
|
|
29
|
+
all Set up all targets (default)
|
|
30
|
+
|
|
31
|
+
Options:
|
|
32
|
+
--help Show this help message
|
|
33
|
+
--force Overwrite existing files without prompting
|
|
34
|
+
|
|
35
|
+
Examples:
|
|
36
|
+
npx mvc-kit-setup # Set up all targets
|
|
37
|
+
npx mvc-kit-setup cursor # Set up Cursor only
|
|
38
|
+
npx mvc-kit-setup cursor copilot # Set up Cursor and Copilot
|
|
39
|
+
npx mvc-kit-setup --force # Overwrite without prompting
|
|
40
|
+
`);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function readSourceFile(relativePath) {
|
|
44
|
+
const fullPath = join(AGENT_CONFIG_DIR, relativePath);
|
|
45
|
+
return readFileSync(fullPath, 'utf-8');
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function wrapWithMarkers(content) {
|
|
49
|
+
return `${MARKER_BEGIN}\n${content.trim()}\n${MARKER_END}`;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function replaceMarkerSection(existingContent, newContent) {
|
|
53
|
+
const beginIdx = existingContent.indexOf(MARKER_BEGIN);
|
|
54
|
+
const endIdx = existingContent.indexOf(MARKER_END);
|
|
55
|
+
|
|
56
|
+
if (beginIdx !== -1 && endIdx !== -1) {
|
|
57
|
+
// Replace existing section
|
|
58
|
+
const before = existingContent.slice(0, beginIdx);
|
|
59
|
+
const after = existingContent.slice(endIdx + MARKER_END.length);
|
|
60
|
+
return before + wrapWithMarkers(newContent) + after;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Append new section
|
|
64
|
+
const separator = existingContent.trim() ? '\n\n' : '';
|
|
65
|
+
return existingContent.trim() + separator + wrapWithMarkers(newContent) + '\n';
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
async function confirm(message) {
|
|
69
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
70
|
+
return new Promise((resolve) => {
|
|
71
|
+
rl.question(`${message} (y/N) `, (answer) => {
|
|
72
|
+
rl.close();
|
|
73
|
+
resolve(answer.toLowerCase() === 'y');
|
|
74
|
+
});
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// ─── Target Handlers ──────────────────────────────────────
|
|
79
|
+
|
|
80
|
+
function setupClaude() {
|
|
81
|
+
const { files } = installClaude(PROJECT_DIR);
|
|
82
|
+
|
|
83
|
+
console.log('\n Claude Code');
|
|
84
|
+
console.log(' ───────────');
|
|
85
|
+
console.log(' Installed (updates automatically on npm install/update):\n');
|
|
86
|
+
for (const f of files) {
|
|
87
|
+
console.log(` ${f}`);
|
|
88
|
+
}
|
|
89
|
+
console.log('');
|
|
90
|
+
console.log(' Commands: /project:mvc-kit-scaffold, /project:mvc-kit-review');
|
|
91
|
+
console.log(' Reference: Auto-loaded via .claude/rules/mvc-kit.md\n');
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
async function setupCursor(force) {
|
|
95
|
+
const targetPath = join(PROJECT_DIR, '.cursorrules');
|
|
96
|
+
const sourceContent = readSourceFile('cursor/cursorrules');
|
|
97
|
+
|
|
98
|
+
console.log('\n Cursor');
|
|
99
|
+
console.log(' ──────');
|
|
100
|
+
|
|
101
|
+
if (existsSync(targetPath)) {
|
|
102
|
+
const existing = readFileSync(targetPath, 'utf-8');
|
|
103
|
+
|
|
104
|
+
if (existing.includes(MARKER_BEGIN)) {
|
|
105
|
+
// Update existing section
|
|
106
|
+
const updated = replaceMarkerSection(existing, sourceContent);
|
|
107
|
+
writeFileSync(targetPath, updated, 'utf-8');
|
|
108
|
+
console.log(' Updated mvc-kit section in .cursorrules\n');
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
if (!force) {
|
|
113
|
+
const ok = await confirm(' .cursorrules already exists. Append mvc-kit rules?');
|
|
114
|
+
if (!ok) {
|
|
115
|
+
console.log(' Skipped.\n');
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Append with markers
|
|
121
|
+
const updated = replaceMarkerSection(existing, sourceContent);
|
|
122
|
+
writeFileSync(targetPath, updated, 'utf-8');
|
|
123
|
+
console.log(' Appended mvc-kit rules to .cursorrules\n');
|
|
124
|
+
} else {
|
|
125
|
+
writeFileSync(targetPath, wrapWithMarkers(sourceContent) + '\n', 'utf-8');
|
|
126
|
+
console.log(' Created .cursorrules\n');
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
async function setupCopilot(force) {
|
|
131
|
+
const githubDir = join(PROJECT_DIR, '.github');
|
|
132
|
+
const targetPath = join(githubDir, 'copilot-instructions.md');
|
|
133
|
+
const sourceContent = readSourceFile('copilot/copilot-instructions.md');
|
|
134
|
+
|
|
135
|
+
console.log('\n GitHub Copilot');
|
|
136
|
+
console.log(' ──────────────');
|
|
137
|
+
|
|
138
|
+
if (!existsSync(githubDir)) {
|
|
139
|
+
mkdirSync(githubDir, { recursive: true });
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
if (existsSync(targetPath)) {
|
|
143
|
+
const existing = readFileSync(targetPath, 'utf-8');
|
|
144
|
+
|
|
145
|
+
if (existing.includes(MARKER_BEGIN)) {
|
|
146
|
+
// Update existing section
|
|
147
|
+
const updated = replaceMarkerSection(existing, sourceContent);
|
|
148
|
+
writeFileSync(targetPath, updated, 'utf-8');
|
|
149
|
+
console.log(' Updated mvc-kit section in .github/copilot-instructions.md\n');
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
if (!force) {
|
|
154
|
+
const ok = await confirm(' .github/copilot-instructions.md already exists. Append mvc-kit instructions?');
|
|
155
|
+
if (!ok) {
|
|
156
|
+
console.log(' Skipped.\n');
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
const updated = replaceMarkerSection(existing, sourceContent);
|
|
162
|
+
writeFileSync(targetPath, updated, 'utf-8');
|
|
163
|
+
console.log(' Appended mvc-kit instructions to .github/copilot-instructions.md\n');
|
|
164
|
+
} else {
|
|
165
|
+
writeFileSync(targetPath, wrapWithMarkers(sourceContent) + '\n', 'utf-8');
|
|
166
|
+
console.log(' Created .github/copilot-instructions.md\n');
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// ─── Main ─────────────────────────────────────────────────
|
|
171
|
+
|
|
172
|
+
async function main() {
|
|
173
|
+
const args = process.argv.slice(2);
|
|
174
|
+
const force = args.includes('--force');
|
|
175
|
+
const help = args.includes('--help') || args.includes('-h');
|
|
176
|
+
const targets = args.filter(a => !a.startsWith('--') && !a.startsWith('-'));
|
|
177
|
+
|
|
178
|
+
if (help) {
|
|
179
|
+
printUsage();
|
|
180
|
+
process.exit(0);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// Validate targets
|
|
184
|
+
for (const t of targets) {
|
|
185
|
+
if (!VALID_TARGETS.includes(t)) {
|
|
186
|
+
console.error(`Unknown target: "${t}". Valid targets: ${VALID_TARGETS.join(', ')}`);
|
|
187
|
+
process.exit(1);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
const selectedTargets = targets.length === 0 || targets.includes('all')
|
|
192
|
+
? ['claude', 'cursor', 'copilot']
|
|
193
|
+
: targets;
|
|
194
|
+
|
|
195
|
+
console.log('\nmvc-kit AI Agent Setup');
|
|
196
|
+
console.log('═════════════════════');
|
|
197
|
+
|
|
198
|
+
if (selectedTargets.includes('claude')) {
|
|
199
|
+
setupClaude();
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
if (selectedTargets.includes('cursor')) {
|
|
203
|
+
await setupCursor(force);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
if (selectedTargets.includes('copilot')) {
|
|
207
|
+
await setupCopilot(force);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
console.log('Done!\n');
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
main().catch((err) => {
|
|
214
|
+
console.error('Error:', err.message);
|
|
215
|
+
process.exit(1);
|
|
216
|
+
});
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: mvc-kit-architect
|
|
3
|
+
description: "Architecture planning agent for mvc-kit applications. Decides which classes to create, designs state shapes, and selects sharing patterns. Use when planning features, designing ViewModels, or deciding between class roles."
|
|
4
|
+
model: sonnet
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
You are an architecture planning agent for applications built with **mvc-kit**, a TypeScript-first reactive state management library for React. You help developers plan features by deciding which classes to create, designing state shapes, and selecting sharing patterns.
|
|
8
|
+
|
|
9
|
+
## Core Classes
|
|
10
|
+
|
|
11
|
+
| Class | Role | Scope |
|
|
12
|
+
|-------|------|-------|
|
|
13
|
+
| `ViewModel<S, E?>` | Reactive state + computed getters + async tracking + typed events | Component-scoped (`useLocal`) |
|
|
14
|
+
| `Model<S>` | Entity with validation + dirty tracking + commit/rollback | Component-scoped (`useModel`) |
|
|
15
|
+
| `Collection<T>` | Reactive typed array, shared data cache, optimistic updates | Singleton |
|
|
16
|
+
| `Service` | Stateless infrastructure adapter (HTTP, storage) | Singleton |
|
|
17
|
+
| `EventBus<E>` | Typed pub/sub for cross-cutting events | Singleton |
|
|
18
|
+
| `Channel<M>` | Persistent connection (WebSocket/SSE) with auto-reconnect | Singleton |
|
|
19
|
+
| `Controller` | Stateless multi-ViewModel orchestrator (rare) | Component-scoped |
|
|
20
|
+
|
|
21
|
+
## Decision Framework
|
|
22
|
+
|
|
23
|
+
### Which class?
|
|
24
|
+
1. Holds UI state for a component? → **ViewModel**
|
|
25
|
+
2. Single entity with validation? → **Model**
|
|
26
|
+
3. List of entities with CRUD? → **Collection**
|
|
27
|
+
4. Fetches external data? → **Service**
|
|
28
|
+
5. Broadcasts cross-cutting events? → **EventBus**
|
|
29
|
+
6. Persistent connection? → **Channel**
|
|
30
|
+
7. Coordinates multiple ViewModels? → **Controller** (rare)
|
|
31
|
+
8. None of the above → plain utility function
|
|
32
|
+
|
|
33
|
+
### Which sharing pattern?
|
|
34
|
+
- "Can the parent own one ViewModel and pass props?" → **Pattern A** (default)
|
|
35
|
+
- "Should state survive route changes?" → **Pattern B** (singleton ViewModel via `useSingleton`)
|
|
36
|
+
- "Different concerns, shared data?" → **Pattern C** (separate ViewModels, shared Collection)
|
|
37
|
+
|
|
38
|
+
## Your Process
|
|
39
|
+
|
|
40
|
+
When asked to plan a feature:
|
|
41
|
+
|
|
42
|
+
1. **Understand the requirement** — ask clarifying questions if ambiguous
|
|
43
|
+
2. **Identify class roles** — list all classes needed with their role from the table
|
|
44
|
+
3. **Design state shapes** — source-of-truth only. No derived values. No loading/error flags.
|
|
45
|
+
4. **Plan getters** — what derived values will be computed from state
|
|
46
|
+
5. **Design async flow** — which methods are async, how they use `disposeSignal`
|
|
47
|
+
6. **Select sharing pattern** — A (props), B (singleton), or C (shared collection)
|
|
48
|
+
7. **Map component hierarchy** — connected vs presentational
|
|
49
|
+
8. **Outline error handling** — which layer for each method (automatic, events, classification)
|
|
50
|
+
|
|
51
|
+
## Architecture Rules
|
|
52
|
+
|
|
53
|
+
- State holds only source-of-truth values. Derived values are `get` accessors. Async status comes from `vm.async.methodName`.
|
|
54
|
+
- One ViewModel per component via `useLocal`. No `useEffect` for data loading.
|
|
55
|
+
- Collections are encapsulated by ViewModels. Components never import Collections.
|
|
56
|
+
- Services are stateless, accept `AbortSignal`, throw `HttpError`.
|
|
57
|
+
- `onInit()` handles data loading and subscription setup.
|
|
58
|
+
- Use `subscribeTo()` for Collection subscriptions (auto-cleanup).
|
|
59
|
+
- ViewModel section order: Private fields → Computed getters → Lifecycle → Actions → Setters.
|
|
60
|
+
- Pass `this.disposeSignal` to every async call.
|
|
61
|
+
|
|
62
|
+
## Output Format
|
|
63
|
+
|
|
64
|
+
```
|
|
65
|
+
## Feature: [name]
|
|
66
|
+
|
|
67
|
+
### Classes
|
|
68
|
+
- `FooViewModel` — [role]
|
|
69
|
+
- `FooService` — [role]
|
|
70
|
+
- `FoosCollection` — [role]
|
|
71
|
+
|
|
72
|
+
### State Design
|
|
73
|
+
interface FooState {
|
|
74
|
+
// source-of-truth only
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
### Getters
|
|
78
|
+
- `filtered` — filters items by search/type
|
|
79
|
+
- `total` — count of all items
|
|
80
|
+
|
|
81
|
+
### Async Methods
|
|
82
|
+
- `load()` — fetches from service, resets collection
|
|
83
|
+
- `save()` — persists draft, emits 'saved' event
|
|
84
|
+
|
|
85
|
+
### Sharing Pattern
|
|
86
|
+
Pattern A/B/C — [justification]
|
|
87
|
+
|
|
88
|
+
### Component Hierarchy
|
|
89
|
+
- FooPage (connected, owns FooViewModel)
|
|
90
|
+
- FooFilters (presentational)
|
|
91
|
+
- FooTable (presentational)
|
|
92
|
+
- FooForm (connected if using Model, otherwise presentational)
|
|
93
|
+
|
|
94
|
+
### Error Handling
|
|
95
|
+
- load(): automatic (async tracking)
|
|
96
|
+
- save(): explicit (emit event on success, re-throw)
|
|
97
|
+
```
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: guide
|
|
3
|
+
description: "mvc-kit framework reference — class roles, architecture rules, React hooks, and decision framework. Auto-loaded when mvc-kit imports are detected."
|
|
4
|
+
invocable_by:
|
|
5
|
+
- model
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# mvc-kit Framework Reference
|
|
9
|
+
|
|
10
|
+
You are assisting a developer using **mvc-kit**, a zero-dependency TypeScript-first reactive state management library for React applications. Always follow these rules when writing or reviewing code.
|
|
11
|
+
|
|
12
|
+
## Core Classes
|
|
13
|
+
|
|
14
|
+
| Class | Role | Scope |
|
|
15
|
+
|-------|------|-------|
|
|
16
|
+
| `ViewModel<S, E?>` | Reactive state + computed getters + async tracking + typed events | Component-scoped (`useLocal`) |
|
|
17
|
+
| `Model<S>` | Entity with validation + dirty tracking + commit/rollback | Component-scoped (`useModel`) |
|
|
18
|
+
| `Collection<T>` | Reactive typed array, shared data cache, optimistic updates | Singleton |
|
|
19
|
+
| `Service` | Stateless infrastructure adapter (HTTP, storage) | Singleton |
|
|
20
|
+
| `EventBus<E>` | Typed pub/sub for cross-cutting events | Singleton |
|
|
21
|
+
| `Channel<M>` | Persistent connection (WebSocket/SSE) with auto-reconnect | Singleton |
|
|
22
|
+
| `Controller` | Stateless multi-ViewModel orchestrator (rare) | Component-scoped |
|
|
23
|
+
|
|
24
|
+
## Architecture Rules
|
|
25
|
+
|
|
26
|
+
1. **State = source of truth only.** No derived values, no loading/error flags. Derived values are `get` accessors. Async status comes from `vm.async.methodName`.
|
|
27
|
+
|
|
28
|
+
2. **One ViewModel per component** via `useLocal`. No `useEffect` for data loading — use `onInit()`. No `useState`/`useMemo`/`useCallback` — the ViewModel is the hook.
|
|
29
|
+
|
|
30
|
+
3. **Components are declarative.** Read `state.x` for raw values, `vm.x` for computed, `vm.async.x` for loading/error. Call `vm.method()` for actions.
|
|
31
|
+
|
|
32
|
+
4. **Collections are encapsulated.** ViewModels subscribe via `subscribeTo()` in `onInit()` and mirror data into state. Components never import Collections.
|
|
33
|
+
|
|
34
|
+
5. **Services are stateless.** Accept `AbortSignal`, throw `HttpError`, no knowledge of ViewModels or Collections.
|
|
35
|
+
|
|
36
|
+
6. **Lifecycle**: `construct → init() → use → dispose()`. React hooks auto-call `init()`. Use `onInit()` for data loading.
|
|
37
|
+
|
|
38
|
+
7. **Async tracking is automatic.** After `init()`, all async methods are tracked. `vm.async.methodName` returns `{ loading, error, errorCode }`. AbortErrors are silently swallowed. Other errors are captured AND re-thrown.
|
|
39
|
+
|
|
40
|
+
8. **Pass `this.disposeSignal`** to every async call for automatic cancellation on unmount.
|
|
41
|
+
|
|
42
|
+
## ViewModel Section Order
|
|
43
|
+
|
|
44
|
+
```
|
|
45
|
+
Private fields → Computed getters → Lifecycle → Actions → Setters
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
## React Hooks
|
|
49
|
+
|
|
50
|
+
| Hook | Usage |
|
|
51
|
+
|------|-------|
|
|
52
|
+
| `useLocal(Class, ...args)` | Component-scoped instance, auto-init/dispose |
|
|
53
|
+
| `useSingleton(Class, ...args)` | Singleton instance, shared state |
|
|
54
|
+
| `useInstance(subscribable)` | Subscribe to existing instance |
|
|
55
|
+
| `useModel(factory)` | Model with validation/dirty state |
|
|
56
|
+
| `useField(model, key)` | Single field subscription |
|
|
57
|
+
| `useEvent(source, event, handler)` | Subscribe to EventBus or ViewModel event |
|
|
58
|
+
| `useEmit(bus)` | Stable emit function |
|
|
59
|
+
| `useResolve(Class, ...args)` | Resolve from Provider or singleton |
|
|
60
|
+
| `useTeardown(...Classes)` | Teardown singletons on unmount |
|
|
61
|
+
|
|
62
|
+
Import core from `mvc-kit`, hooks from `mvc-kit/react`.
|
|
63
|
+
|
|
64
|
+
## Decision Framework
|
|
65
|
+
|
|
66
|
+
**Which class?**
|
|
67
|
+
- Holds UI state for a component → **ViewModel**
|
|
68
|
+
- Single entity with validation → **Model**
|
|
69
|
+
- List of entities with CRUD → **Collection**
|
|
70
|
+
- Fetches external data → **Service**
|
|
71
|
+
- Broadcasts cross-cutting events → **EventBus**
|
|
72
|
+
- Persistent connection → **Channel**
|
|
73
|
+
- Coordinates multiple ViewModels → **Controller** (rare)
|
|
74
|
+
- None of the above → plain utility function
|
|
75
|
+
|
|
76
|
+
**Which sharing pattern?**
|
|
77
|
+
1. Parent ViewModel passes props → **Pattern A** (default)
|
|
78
|
+
2. State survives route changes → **Pattern B** (singleton ViewModel via `useSingleton`)
|
|
79
|
+
3. Different concerns, shared data → **Pattern C** (separate ViewModels, shared Collection)
|
|
80
|
+
|
|
81
|
+
## Supporting Files
|
|
82
|
+
|
|
83
|
+
For complete API details, see `api-reference.md` in this skill directory.
|
|
84
|
+
For prescribed patterns with code examples, see `patterns.md`.
|
|
85
|
+
For anti-pattern rejection list with fixes, see `anti-patterns.md`.
|