# Srtd Dev

> 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.

- Skill: `majiayu000/srtd-dev` (Agent Skill, multi-file: 2 files)
- Install (CLI): `npx skillmds add majiayu000/srtd-dev`
- Raw SKILL.md: https://api.skillmd.com/api/skills/majiayu000/srtd-dev/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Coding & Dev Tools
- Author: majiayu000 (https://skillmd.com/u/majiayu000)
- Updated: 2026-09-09
- Page: https://skillmd.com/skills/majiayu000/srtd-dev

---


# 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.

