
service-audit
by marcusta
SKILL.md
name: service-audit description: Enforce TapScore backend service layer organization and code quality standards. Use when implementing or modifying service classes, business logic, or database operations. Ensures compliance with service layer patterns from BACKEND_GUIDE.md.
TapScore Service Layer Audit Skill
This skill enforces TapScore's backend service layer organization and code quality standards. Use for ANY backend service work: new services, business logic, database operations, or refactoring.
Implementation Workflow
Copy this checklist and track your progress:
Service Implementation Progress:
- [ ] Step 1: Read backend architecture patterns
- [ ] Step 2: Organize methods by category
- [ ] Step 3: Implement following separation rules
- [ ] Step 4: Validate service layer compliance
- [ ] Step 5: Check code quality metrics
Step 1: Read Backend Architecture Patterns
MANDATORY - Read this file before coding:
cat docs/backend/BACKEND_GUIDE.md # Service layer organization patterns
What to extract:
- Service layer organization (query vs logic vs public methods)
- Transaction handling patterns
- Method naming conventions
- Transform patterns (Row to Domain types)
Step 2: Organize Methods by Category
Core Rule
A method contains EITHER a single SQL query OR business logic - never both.
Method Categories
1. Query Methods (private, single SQL statement):
- Prefix:
find*,get*,insert*,update*,delete* - Contains: Exactly one SQL query
- Transformation: Minimal (JSON.parse, type casting only)
- Transaction: Automatically joins active transaction
// ✅ CORRECT - Single query, minimal transform
private findCompetitionById(id: number): Competition | null {
return this.db.prepare("SELECT * FROM competitions WHERE id = ?")
.get(id) as Competition | null;
}
// ❌ WRONG - Query + business logic mixed
private findCompetitionById(id: number): Competition | null {
const comp = this.db.prepare("SELECT * FROM competitions WHERE id = ?")
.get(id) as Competition | null;
if (comp && comp.is_finished) {
// Business logic doesn't belong here
this.calculateFinalStandings(comp.id);
}
return comp;
}
2. Logic Methods (private, no SQL):
- Prefix:
calculate*,build*,validate*,process*,transform* - Contains: Pure business logic only
- Database access: None (calls logic methods only, never query methods)
- Testing: Easily unit-testable without database
// ✅ CORRECT - Pure logic, no database
private calculateRelativeToPar(score: number[], pars: number[]): number {
return score.reduce((rel, shots, i) =>
shots > 0 ? rel + (shots - pars[i]) : rel, 0);
}
// ❌ WRONG - Logic method accessing database
private calculateLeaderboard(competitionId: number): LeaderboardEntry[] {
const participants = this.db.prepare("SELECT * FROM participants...")
.all() as Participant[]; // Database access in logic method
return participants.map(p => this.processEntry(p));
}
3. Transform Methods (private, Row to Domain):
- Purpose: Convert database rows to domain types
- Must have: Explicit return type annotations
- Handles: Type conversions (0/1 → boolean, JSON strings → arrays)
// ✅ CORRECT - Explicit return type, handles conversions
private transformParticipantRow(row: ParticipantRow): Participant {
return {
...row,
score: JSON.parse(row.score), // JSON string → array
is_locked: Boolean(row.is_locked), // SQLite 0/1 → boolean
};
}
// ❌ WRONG - No return type, missing conversions
private transformParticipantRow(row: ParticipantRow) {
return row; // Returns raw database representation
}
4. Public API Methods (orchestration):
- Purpose: Combines query + logic methods
- Defines: Transaction boundaries
- Validates: Input before starting transactions
// ✅ CORRECT - Orchestrates query and logic methods
async getLeaderboard(competitionId: number): Promise<LeaderboardEntry[]> {
// Query method
const competition = this.findCompetitionById(competitionId);
if (!competition) throw new Error("Competition not found");
// Query method
const participants = this.findParticipantsByCompetition(competitionId);
// Logic method
const pars = this.parseParsArray(competition.pars);
// Logic method + sorting
return participants
.map(p => this.calculateEntryScore(p, pars))
.sort((a, b) => a.relativeToPar - b.relativeToPar);
}
Step 3: Implement Following Separation Rules
Transaction Handling
Pattern: Validate OUTSIDE transaction, execute INSIDE
// ✅ CORRECT - Validation before transaction
createCompetitionWithTeeTimes(data: CreateDto, teeTimes: string[]) {
// Fail fast - validate before starting transaction
this.validateCompetitionData(data);
this.validateTeeTimes(teeTimes);
// Transaction boundary - all query methods inside automatically join
return this.db.transaction(() => {
const competition = this.insertCompetition(data);
const createdTeeTimes = teeTimes.map(time =>
this.insertTeeTime(competition.id, time)
);
return { competition, teeTimes: createdTeeTimes };
})();
}
// ❌ WRONG - Validation inside transaction
createCompetitionWithTeeTimes(data: CreateDto, teeTimes: string[]) {
return this.db.transaction(() => {
// Wasted transaction if validation fails
this.validateCompetitionData(data);
this.validateTeeTimes(teeTimes);
const competition = this.insertCompetition(data);
// ...
})();
}
Player Display Names
CRITICAL: Always prefer player_profiles.display_name over players.name
// ✅ CORRECT - Query includes display_name with fallback
private findParticipantsWithPlayers(competitionId: number): ParticipantRow[] {
return this.db.prepare(`
SELECT
p.*,
pl.name as player_name,
pp.display_name as player_display_name
FROM participants p
LEFT JOIN players pl ON p.player_id = pl.id
LEFT JOIN player_profiles pp ON pl.id = pp.player_id
WHERE p.competition_id = ?
`).all(competitionId) as ParticipantRow[];
}
// Transform uses helper
import { getPlayerDisplayName } from "../utils/player-display";
private transformParticipantRow(row: ParticipantRow): Participant {
return {
...row,
playerName: getPlayerDisplayName(
row.player_display_name,
row.player_name
),
};
}
Use Golf Constants
import { GOLF } from "../constants/golf";
// ✅ CORRECT - Uses constants
if (holesPlayed === GOLF.HOLES_PER_ROUND) {
const slopeRating = tee.slope_rating || GOLF.STANDARD_SLOPE_RATING;
// ...
}
// ❌ WRONG - Magic numbers
if (holesPlayed === 18) {
const slopeRating = tee.slope_rating || 113;
// ...
}
Step 4: Validate Service Layer Compliance
Method Organization:
- Query methods are private and contain exactly one SQL query
- Query methods have prefixes:
find*,get*,insert*,update*,delete* - Logic methods are private and contain NO database access
- Logic methods have prefixes:
calculate*,build*,validate*,process*,transform* - Transform methods have explicit return type annotations
- Public API methods orchestrate query + logic methods
- Public API methods define transaction boundaries
Transaction Handling:
- Validation happens BEFORE
db.transaction() - All related queries within single
db.transaction(() => { ... })() - Transaction used only when multiple queries need atomicity
- Single-query operations don't use explicit transactions (auto-commit)
Data Patterns:
- Player queries include
player_profiles.display_name - Display names use
getPlayerDisplayName()helper - Used
GOLFconstants instead of magic numbers - JSON.parse wrapped with error handling
Step 5: Check Code Quality Metrics
Method Size Limits:
- Logic methods under 50 lines (excluding blanks/comments)
- Public API methods under 50 lines
- Query methods: no strict limit (SQL can be long, wrapper minimal)
Control Flow Nesting:
- Maximum 3 levels of nesting
- Used early returns to flatten code
Variable Naming:
- Booleans prefixed:
is*,has*,should*,can* - Collections are plural:
participants,scores,teeTimes - Transformed data named descriptively:
competitionWithCourse,sortedLeaderboard - No generic names:
data,result,item,temp
Type Safety:
- NO use of
anytype - Explicit return types on all methods
- Type guards or validation before type assertions
Common Anti-Patterns to Avoid
-
❌ Mixed query + logic in one method
- Query methods should contain ONLY SQL + type cast
- Logic methods should contain NO database access
-
❌ Validation inside transactions
- Always validate BEFORE starting transaction
- Fail fast to avoid wasted transaction overhead
-
❌ Missing return type annotations
- Transform methods MUST have explicit return types
- Catches schema drift at compile time
-
❌ Calling query methods from logic methods
- Logic methods should only call other logic methods
- Maintains testability without database
-
❌ Using player name without display_name fallback
- Always query both
players.nameandplayer_profiles.display_name - Always prefer display_name when available
- Always query both
-
❌ Magic numbers instead of GOLF constants
- Use
GOLF.HOLES_PER_ROUNDnot18 - Use
GOLF.STANDARD_SLOPE_RATINGnot113
- Use
-
❌ Naked JSON.parse without error handling
- Wrap JSON.parse in try/catch
- Provide descriptive error messages
-
❌ Deeply nested control flow (> 3 levels)
- Use early returns:
if (!condition) return; - Extract complex logic to helper methods
- Use early returns:
Examples from BACKEND_GUIDE.md
Good Service Organization:
class CompetitionService {
// Query method - single SQL query
private findCompetitionById(id: number): Competition | null {
return this.db.prepare("SELECT * FROM competitions WHERE id = ?")
.get(id) as Competition | null;
}
// Logic method - pure calculation
private calculateRelativeToPar(score: number[], pars: number[]): number {
return score.reduce((rel, shots, i) =>
shots > 0 ? rel + (shots - pars[i]) : rel, 0);
}
// Transform method - explicit return type
private transformCompetitionRow(row: CompetitionRow): Competition {
return {
...row,
pars: JSON.parse(row.pars),
is_finished: Boolean(row.is_finished),
};
}
// Public API method - orchestrates
async getLeaderboard(id: number): Promise<LeaderboardEntry[]> {
const comp = this.findCompetitionById(id);
if (!comp) throw new Error("Competition not found");
const participants = this.findParticipantsByCompetition(id);
const pars = JSON.parse(comp.pars);
return participants
.map(p => this.calculateEntryScore(p, pars))
.sort((a, b) => a.relativeToPar - b.relativeToPar);
}
}
Good Transaction Pattern:
createCompetitionWithTeeTimes(data: CreateDto, teeTimes: string[]) {
// Validation OUTSIDE transaction
this.validateCompetitionData(data);
return this.db.transaction(() => {
const competition = this.insertCompetition(data);
const createdTeeTimes = teeTimes.map(time =>
this.insertTeeTime(competition.id, time)
);
return { competition, teeTimes: createdTeeTimes };
})();
}
Summary
TapScore service layer approach: Clear separation between queries and logic, explicit transaction boundaries, and rigorous type safety. Quality through organization and discipline.
Every service implementation must:
- Read BACKEND_GUIDE.md first
- Separate query methods from logic methods
- Validate before transactions
- Use explicit return types on transforms
- Follow naming conventions
- Use GOLF constants
- Handle player display names correctly
- Stay under method size limits
- Keep nesting under 3 levels
Build services that are maintainable, testable, and reliable.
スコア
総合スコア
リポジトリの品質指標に基づく評価
SKILL.mdファイルが含まれている
ライセンスが設定されている
100文字以上の説明がある
GitHub Stars 100以上
3ヶ月以内に更新がある
10回以上フォークされている
オープンIssueが50未満
プログラミング言語が設定されている
1つ以上のタグが設定されている
レビュー
レビュー機能は近日公開予定です