@bradtaylorsf/alpha-loop 1.1.0 → 1.1.2

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,229 @@
1
+ ---
2
+ name: sqlite-patterns
3
+ description: SQLite database patterns using better-sqlite3. Synchronous API, file-based storage, great for single-user apps and development.
4
+ ---
5
+
6
+ # SQLite Patterns Skill
7
+
8
+ Patterns for SQLite database operations using better-sqlite3.
9
+
10
+ ## When to Use SQLite
11
+
12
+ - Single-user applications
13
+ - Development/prototyping
14
+ - File-based storage needs
15
+ - Simple applications without concurrency
16
+ - Embedded databases
17
+
18
+ ## Setup
19
+
20
+ ```typescript
21
+ import Database from 'better-sqlite3';
22
+
23
+ // Production
24
+ const db = new Database('./data/app.db');
25
+
26
+ // Testing (in-memory)
27
+ const testDb = new Database(':memory:');
28
+
29
+ // Enable WAL mode for better performance
30
+ db.pragma('journal_mode = WAL');
31
+ ```
32
+
33
+ ## Basic Patterns
34
+
35
+ ### Query Single Row
36
+
37
+ ```typescript
38
+ function getUserById(id: number): User | null {
39
+ const result = db.prepare(`
40
+ SELECT id, email, name, created_at as createdAt
41
+ FROM users
42
+ WHERE id = ?
43
+ `).get(id);
44
+
45
+ return result ?? null; // better-sqlite3 returns undefined, not null
46
+ }
47
+ ```
48
+
49
+ ### Query Multiple Rows
50
+
51
+ ```typescript
52
+ function getAllUsers(): User[] {
53
+ return db.prepare(`
54
+ SELECT id, email, name, created_at as createdAt
55
+ FROM users
56
+ ORDER BY created_at DESC
57
+ `).all() as User[];
58
+ }
59
+ ```
60
+
61
+ ### Insert and Get ID
62
+
63
+ ```typescript
64
+ function createUser(data: CreateUserDto): User {
65
+ const stmt = db.prepare(`
66
+ INSERT INTO users (email, password_hash, name)
67
+ VALUES (?, ?, ?)
68
+ `);
69
+
70
+ const info = stmt.run(data.email, passwordHash, data.name);
71
+ const id = info.lastInsertRowid;
72
+
73
+ return getUserById(Number(id))!;
74
+ }
75
+ ```
76
+
77
+ ### Update
78
+
79
+ ```typescript
80
+ function updateUser(id: number, updates: UpdateUserDto): User | null {
81
+ const stmt = db.prepare(`
82
+ UPDATE users
83
+ SET email = COALESCE(?, email),
84
+ name = COALESCE(?, name),
85
+ updated_at = datetime('now')
86
+ WHERE id = ?
87
+ `);
88
+
89
+ const info = stmt.run(updates.email, updates.name, id);
90
+
91
+ if (info.changes === 0) return null;
92
+ return getUserById(id);
93
+ }
94
+ ```
95
+
96
+ ### Delete
97
+
98
+ ```typescript
99
+ function deleteUser(id: number): boolean {
100
+ const info = db.prepare('DELETE FROM users WHERE id = ?').run(id);
101
+ return info.changes > 0;
102
+ }
103
+ ```
104
+
105
+ ## Transactions
106
+
107
+ ```typescript
108
+ function transferFunds(fromId: number, toId: number, amount: number): void {
109
+ const transfer = db.transaction(() => {
110
+ db.prepare('UPDATE accounts SET balance = balance - ? WHERE id = ?')
111
+ .run(amount, fromId);
112
+
113
+ db.prepare('UPDATE accounts SET balance = balance + ? WHERE id = ?')
114
+ .run(amount, toId);
115
+ });
116
+
117
+ transfer(); // Automatically rolls back on error
118
+ }
119
+ ```
120
+
121
+ ## JSON Fields
122
+
123
+ ```typescript
124
+ // Store JSON
125
+ function saveSettings(userId: number, settings: Settings): void {
126
+ db.prepare(`
127
+ UPDATE users SET settings = ? WHERE id = ?
128
+ `).run(JSON.stringify(settings), userId);
129
+ }
130
+
131
+ // Parse JSON
132
+ function getSettings(userId: number): Settings {
133
+ const result = db.prepare(`
134
+ SELECT settings FROM users WHERE id = ?
135
+ `).get(userId) as { settings: string } | undefined;
136
+
137
+ return result ? JSON.parse(result.settings) : {};
138
+ }
139
+
140
+ // Helper for safe JSON parsing
141
+ function parseJsonField<T>(value: string | null | undefined, fallback: T): T {
142
+ if (!value) return fallback;
143
+ try {
144
+ return JSON.parse(value) as T;
145
+ } catch {
146
+ return fallback;
147
+ }
148
+ }
149
+ ```
150
+
151
+ ## Migrations
152
+
153
+ ```sql
154
+ -- migrations/001_create_users.sql
155
+ CREATE TABLE IF NOT EXISTS users (
156
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
157
+ email TEXT UNIQUE NOT NULL,
158
+ password_hash TEXT NOT NULL,
159
+ name TEXT NOT NULL,
160
+ settings TEXT DEFAULT '{}',
161
+ created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
162
+ updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
163
+ );
164
+
165
+ CREATE INDEX idx_users_email ON users(email);
166
+ ```
167
+
168
+ ```typescript
169
+ // Run migrations
170
+ function runMigrations(db: Database.Database): void {
171
+ const migrations = fs.readdirSync('./migrations')
172
+ .filter(f => f.endsWith('.sql'))
173
+ .sort();
174
+
175
+ for (const file of migrations) {
176
+ const sql = fs.readFileSync(`./migrations/${file}`, 'utf-8');
177
+ db.exec(sql);
178
+ }
179
+ }
180
+ ```
181
+
182
+ ## Error Handling
183
+
184
+ ```typescript
185
+ function createUser(data: CreateUserDto): User {
186
+ try {
187
+ // ... insert logic
188
+ } catch (error: unknown) {
189
+ if (error instanceof Error && 'code' in error) {
190
+ const sqliteError = error as { code: string };
191
+ if (sqliteError.code === 'SQLITE_CONSTRAINT') {
192
+ throw new AppError('Email already exists', 400);
193
+ }
194
+ }
195
+ throw error;
196
+ }
197
+ }
198
+ ```
199
+
200
+ ## Timestamps
201
+
202
+ ```typescript
203
+ // SQLite datetime format
204
+ const now = new Date().toISOString(); // '2024-01-15T10:30:00.000Z'
205
+
206
+ // Or use SQLite's built-in
207
+ db.prepare(`
208
+ INSERT INTO logs (message, created_at)
209
+ VALUES (?, datetime('now'))
210
+ `).run(message);
211
+ ```
212
+
213
+ ## Best Practices
214
+
215
+ 1. **Use prepared statements** - Prevents SQL injection
216
+ 2. **Use transactions** - For multiple related operations
217
+ 3. **Enable WAL mode** - Better concurrent read performance
218
+ 4. **Use in-memory for tests** - Fast, isolated testing
219
+ 5. **Handle undefined** - better-sqlite3 returns undefined, not null
220
+ 6. **Index foreign keys** - Improve JOIN performance
221
+ 7. **Use COALESCE for updates** - Partial updates without overwriting
222
+
223
+ ## Common Gotchas
224
+
225
+ - Returns `undefined` not `null` for missing rows
226
+ - `lastInsertRowid` is a BigInt (convert with Number())
227
+ - JSON must be stringified before storage
228
+ - Datetime stored as TEXT (ISO format recommended)
229
+ - No native boolean type (use INTEGER 0/1)
@@ -0,0 +1,99 @@
1
+ ---
2
+ name: test-caching
3
+ description: API response caching for expensive AI API calls in tests. Use when writing tests that call Claude, OpenAI, or other AI APIs.
4
+ auto_load: false
5
+ priority: medium
6
+ ---
7
+
8
+ # Test Caching Skill
9
+
10
+ ## Trigger
11
+ When writing tests that make HTTP calls to AI/LLM APIs (Claude, OpenAI, etc.) or any expensive external API.
12
+
13
+ ## How It Works
14
+
15
+ The `mockExpensiveAPI()` helper intercepts HTTP requests matching a URL pattern.
16
+
17
+ - **Default mode** (`pnpm test`): Replays responses from fixture files in `tests/fixtures/`. No network calls.
18
+ - **Record mode** (`RECORD_FIXTURES=true` / `pnpm test:full`): Lets real requests through, captures responses to fixture files.
19
+
20
+ ## Usage
21
+
22
+ ```typescript
23
+ import { mockExpensiveAPI } from '../../src/testing/cache';
24
+
25
+ describe('my AI feature', () => {
26
+ let mock: ReturnType<typeof mockExpensiveAPI>;
27
+
28
+ beforeEach(() => {
29
+ mock = mockExpensiveAPI({
30
+ name: 'my-feature-openai', // fixture filename
31
+ pattern: 'https://api.openai.com', // URL prefix to intercept
32
+ service: 'openai', // metadata
33
+ estimatedCostUSD: 0.02, // metadata
34
+ });
35
+ });
36
+
37
+ afterEach(() => {
38
+ mock.restore(); // always restore HTTP patches
39
+ });
40
+
41
+ it('calls the AI API', async () => {
42
+ // In replay mode, this returns the cached response
43
+ // In record mode, this hits the real API and saves the response
44
+ const result = await callMyAIFeature();
45
+ expect(result).toBeDefined();
46
+ });
47
+ });
48
+ ```
49
+
50
+ ## Options
51
+
52
+ | Option | Required | Description |
53
+ |--------|----------|-------------|
54
+ | `name` | Yes | Unique fixture name (becomes `<name>.fixture.json`) |
55
+ | `pattern` | Yes | URL prefix (string) or RegExp to intercept |
56
+ | `service` | No | Service name for metadata (default: `"unknown"`) |
57
+ | `estimatedCostUSD` | No | Cost per call for metadata (default: `0`) |
58
+ | `fixturesDir` | No | Override fixture directory (default: `tests/fixtures/`) |
59
+
60
+ ## Commands
61
+
62
+ | Command | Description |
63
+ |---------|-------------|
64
+ | `pnpm test` | Run tests with cached API responses (default) |
65
+ | `pnpm test:full` | Run tests with real API calls, re-record fixtures |
66
+
67
+ ## Fixture Format
68
+
69
+ Fixtures are stored as JSON arrays in `tests/fixtures/<name>.fixture.json`:
70
+
71
+ ```json
72
+ [
73
+ {
74
+ "request": { "url": "https://api.openai.com/v1/chat", "method": "POST", "body": { "model": "gpt-4" } },
75
+ "response": { "status": 200, "headers": {}, "body": { "choices": [] } },
76
+ "metadata": { "recordedAt": "2025-01-15T10:00:00.000Z", "service": "openai", "estimatedCostUSD": 0.02 }
77
+ }
78
+ ]
79
+ ```
80
+
81
+ ## Cache Invalidation
82
+
83
+ Fixtures older than 30 days produce a warning at test startup. Re-record with `RECORD_FIXTURES=true` to refresh.
84
+
85
+ ## In the Loop
86
+
87
+ `scripts/loop.sh` uses cached mode by default. Pass `--run-full` to bypass cache:
88
+
89
+ ```bash
90
+ bash scripts/loop.sh --run-full # real API calls
91
+ bash scripts/loop.sh # cached (default)
92
+ ```
93
+
94
+ ## Rules
95
+
96
+ 1. Always call `mock.restore()` in `afterEach` to undo HTTP patches
97
+ 2. Commit fixture files to git so CI can replay without API keys
98
+ 3. Use descriptive `name` values — one fixture per test scenario
99
+ 4. Check `mock.warnings` for staleness alerts in your test setup