srtd-dev by t1mmen
Expert knowledge for developing the SRTD codebase itself. Use when implementing features, fixing bugs, understanding architecture, or writing tests for SRTD internals. NOT for end users of srtd CLI.
Content & Writing
100 Stars
7 Forks
Updated Dec 31, 2025, 07:10 AM
Why Use This
This skill provides specialized capabilities for t1mmen's codebase.
Use Cases
- Developing new features in the t1mmen repository
- Refactoring existing code to follow t1mmen standards
- Understanding and working with t1mmen's codebase structure
Install Guide
2 steps- 1
Skip this step if Ananke is already installed.
- 2
Skill Snapshot
Auto scan of skill assets. Informational only.
Valid SKILL.md
Checks against SKILL.md specification
Source & Community
Skill Stats
SKILL.md 329 Lines
Total Files 1
Total Size 0 B
License NOASSERTION
---
name: srtd-dev
description: Expert knowledge for developing the SRTD codebase itself. Use when implementing features, fixing bugs, understanding architecture, or writing tests for SRTD internals. NOT for end users of srtd CLI.
---
# SRTD Development Skill
Expert guidance for working with the SRTD codebase - a CLI tool for live-reloading SQL templates into Supabase local databases.
## Quick Reference
### Key Commands
```bash
npm test # Run all tests
npx vitest run -t "pattern" # Run specific test
npm run typecheck # Type check
npm run lint # Biome lint + fix
npm start -- watch # Run watch command
npm run supabase:start # Start test database
```
### Key Files by Task
| Task | Primary Files |
|------|---------------|
| Add CLI option | `src/commands/{command}.ts`, `src/cli.ts` |
| Modify template processing | `src/services/Orchestrator.ts` |
| Change state tracking | `src/services/StateService.ts` |
| Fix database issues | `src/services/DatabaseService.ts` |
| Modify migration output | `src/services/MigrationBuilder.ts` |
| Change file watching | `src/services/FileSystemService.ts` |
| Update config | `src/utils/config.ts`, `src/types.ts` |
## Architecture Mental Model
**Unidirectional flow** - data flows one direction through the system:
```
File Change → FileSystemService → Orchestrator → StateService
↓
DatabaseService / MigrationBuilder
↓
StateService (update) → Event Emission
```
### Service Boundaries (Critical)
**FileSystemService** owns:
- Template discovery (glob matching)
- File watching (Chokidar, 100ms debounce)
- File I/O (read, write, rename)
- Hash computation (MD5)
**StateService** owns:
- All state mutations (single source of truth)
- Build log persistence (`.buildlog.json`, `.buildlog.local.json`)
- State machine transitions (UNSEEN → CHANGED → APPLIED/BUILT → SYNCED)
- Hash comparison for change detection
**DatabaseService** owns:
- Connection pooling (pg.Pool, max 10 connections)
- Retry logic (3 attempts, exponential backoff)
- Error categorization (CONNECTION_ERROR, SYNTAX_ERROR, etc.)
- Transaction management (BEGIN/COMMIT/ROLLBACK)
- Advisory locks per template
**MigrationBuilder** owns:
- Timestamp generation (increments from buildLog.lastTimestamp)
- Migration file formatting (banner, footer, transaction wrap)
- Bundle mode (multiple templates → single file)
**Orchestrator** owns:
- Service coordination (does NOT own state)
- Queue management (processQueue, pendingRecheck)
- Event emission (templateChanged, templateApplied, templateError)
- Command execution (apply, build, watch)
### Key Design Decisions
1. **Dual build logs**: `.buildlog.json` (what was built, commit) + `.buildlog.local.json` (what was applied, gitignore)
2. **Hash-based change detection**: `currentHash !== lastAppliedHash && currentHash !== lastBuiltHash`
3. **EventEmitter pattern**: Loose coupling between services
4. **Disposable pattern**: `await using` for automatic cleanup
5. **Queue-based processing**: FIFO with recheck for modified templates
## Debugging Workflows
### Template Not Processing
```typescript
// Check 1: Is template being found?
// FileSystemService.findTemplates() uses glob pattern from config.filter
// Check 2: Is hash comparison returning false?
// StateService.hasTemplateChanged() compares against BOTH build logs
// Check 3: Is it a WIP template?
// isWipTemplate() checks for config.wipIndicator suffix (.wip.sql)
// Debugging: Add to Orchestrator.processTemplate():
console.log({
path,
hash: currentHash,
state: this.stateService.getTemplateStatus(path)
});
```
### Database Connection Errors
```typescript
// DatabaseService categorizes errors via DatabaseErrorType:
// - CONNECTION_ERROR: ECONNREFUSED, ENOTFOUND, ECONNRESET
// - POOL_EXHAUSTED: "pool is exhausted", "too many clients"
// - TIMEOUT_ERROR: ETIMEOUT or timeout in message
// Check pool status:
console.log({
total: pool.totalCount,
idle: pool.idleCount,
waiting: pool.waitingCount
});
```
### State Machine Issues
```typescript
// Valid transitions in StateService:
// UNSEEN → CHANGED
// CHANGED → APPLIED, BUILT, ERROR
// APPLIED → CHANGED, SYNCED
// BUILT → CHANGED, SYNCED
// SYNCED → CHANGED
// ERROR → CHANGED
// Check current state:
const info = stateService.templateStates.get(absolutePath);
console.log({ state: info?.state, lastAppliedHash, lastBuiltHash });
```
## Testing Patterns
### Command Tests
```typescript
import { setupCommandTestSpies, createMockUiModule } from '../helpers/testUtils.js';
beforeEach(() => {
vi.clearAllMocks();
vi.resetModules(); // Critical: reload modules
spies = setupCommandTestSpies();
});
afterEach(() => spies.cleanup());
it('handles success', async () => {
const { buildCommand } = await import('../commands/build.js');
mockOrchestrator.build.mockResolvedValue({ built: ['file.sql'], errors: [] });
await buildCommand.parseAsync(['node', 'test']);
spies.assertNoStderr(); // Catch Commander parse errors
expect(spies.exitSpy).toHaveBeenCalledWith(0);
});
```
### Service Tests with TestResource
```typescript
import { createTestResource } from '../helpers/index.js';
it('applies template to database', async () => {
using resources = await createTestResource({ prefix: 'apply' });
await resources.setup();
// Create template with unique function name
const templatePath = await resources.createTemplateWithFunc('test', '_v1');
// Execute within transaction for isolation
const result = await resources.withTransaction(async (client) => {
// ... test logic
return client.query('SELECT ...');
});
// Verify function exists
expect(await resources.verifyFunctionExists()).toBe(true);
// Auto-cleanup via Symbol.asyncDispose
});
```
### Mock Patterns
```typescript
// Mock Orchestrator (most common)
vi.mock('../services/Orchestrator.js', () => ({
Orchestrator: {
create: vi.fn().mockResolvedValue({
apply: vi.fn().mockResolvedValue({ applied: [], errors: [], skipped: [] }),
build: vi.fn().mockResolvedValue({ built: [], errors: [], skipped: [] }),
watch: vi.fn().mockResolvedValue(undefined),
[Symbol.asyncDispose]: vi.fn(),
}),
},
}));
// Mock config
vi.mock('../utils/config.js', () => ({
getConfig: vi.fn().mockResolvedValue({
templateDir: '/tmp/templates',
migrationDir: '/tmp/migrations',
// ... other config
}),
}));
```
## Adding New Features
### New CLI Option
1. Add option to command in `src/commands/{command}.ts`:
```typescript
.option('-x, --example', 'Description')
```
2. Pass to orchestrator method:
```typescript
const result = await orchestrator.apply({ force, example: options.example });
```
3. Handle in orchestrator:
```typescript
async apply(options: ApplyOptions & { example?: boolean }) {
if (options.example) { /* ... */ }
}
```
4. Add test:
```typescript
it('respects --example flag', async () => {
await command.parseAsync(['node', 'test', '--example']);
expect(mockOrchestrator.apply).toHaveBeenCalledWith(
expect.objectContaining({ example: true })
);
});
```
### New Service Method
1. Define interface in `src/types.ts`
2. Implement in service class
3. Expose via Orchestrator if needed
4. Add unit test for service
5. Add integration test for full flow
### New Event Type
1. Define event type in Orchestrator:
```typescript
type OrchestratorEvents = {
newEvent: [payload: NewEventPayload];
// ... existing events
};
```
2. Emit from appropriate location:
```typescript
this.emit('newEvent', payload);
```
3. Listen in command:
```typescript
orchestrator.on('newEvent', (payload) => {
// Update UI
});
```
## Error Handling Patterns
### Service Layer
```typescript
// Categorize and wrap errors
try {
await pool.query(sql);
} catch (error) {
const dbError = this.categorizeError(error);
this.emit('sql:error', { error: dbError });
throw dbError;
}
```
### Command Layer
```typescript
try {
const result = await orchestrator.apply();
process.exit(result.errors.length > 0 ? 1 : 0);
} catch (error) {
console.log(chalk.red(getErrorMessage(error)));
process.exit(1);
}
```
### Interactive Commands
```typescript
try {
const answer = await select({ /* ... */ });
} catch (error) {
if (isPromptExit(error)) {
process.exit(0); // Ctrl+C is clean exit
}
throw error;
}
```
## Common Pitfalls
1. **Forgetting vi.resetModules()** - Command tests fail silently without it
2. **Not capturing console.error** - Commander writes parse errors to stderr
3. **Direct state mutation** - Always use StateService methods, never modify directly
4. **Missing transaction cleanup** - Use `using` pattern or explicit dispose
5. **Testing with shared state** - Use TestResource for isolation
6. **Forgetting Symbol.asyncDispose** - Orchestrator requires async disposal
## Validation Before Commit
```bash
npm run typecheck && npm run lint && npm test
```
All three must pass. CI runs on Node 20.x and 22.x with PostgreSQL 15.
Name Size