Files
AgentCompany/builtin-skills/software-engineer/clean-architecture/references/adapt-mapper-translation.md
T
ZacharyZhang-NY e74dafcd36 Silence render-disposed spam, unbreak bundled skills, drop board wake
Three independently ugly bugs that all fed the same log flood:

* Electron's `WebFrameMain.send` writes every "Render frame was disposed"
  to stderr via `console.error`. When the worker fans a second
  agent-event past a reloading renderer, we produced thousands of
  identical lines and stalled the main loop on stderr I/O. Added a
  `mainFrame.detached` guard so the send is short-circuited before
  Electron's own catch block fires, and installed a targeted filter in
  `suppress-warnings.ts` (now actually imported from the entry point)
  so the specific `console.error("Error sending from webFrameMain:"…)`
  noise never reaches the terminal. Any other payload still surfaces.

* `dispatchAgentMessage` accepted the human-reader sentinel as a wake
  target when an agent replied to `__board__` — `wakeAgentIfPossible`
  then called `db.getAgent("__board__")`, threw, and the dispatcher
  kept repeating on every send. Strip `HUMAN_SENDER_ID` at
  `resolveMessageWakeTargets`, and keep a defense-in-depth guard in the
  heartbeat so a stray caller (future automation fan-out) cannot
  reintroduce the crash.

* Pi's skills loader rejects any SKILL.md whose frontmatter `name`
  diverges from its parent directory. The four bundled `my-writing-style`
  copies declared `name: writing-style-skill`, so every install emitted
  a warning and dropped the skill. They also lived as orphan gitlinks
  (mode 160000 without .gitmodules), and the global `*.md` gitignore
  hid every other SKILL.md from version control. Fixed the frontmatter
  to match the dir, converted the dangling submodules to plain trees,
  and allowlisted `builtin-skills/**/*.md` so a fresh clone carries the
  catalogue instead of silently losing it.
2026-04-20 08:13:48 -04:00

120 lines
2.7 KiB
Markdown

---
title: Use Mappers to Translate Between Layers
impact: MEDIUM
impactDescription: decouples domain from persistence and presentation models
tags: adapt, mapper, translation, layers
---
## Use Mappers to Translate Between Layers
Mappers translate between domain entities and external representations (database rows, API responses, DTOs). Each layer has its own model; mappers bridge them.
**Incorrect (domain entity used everywhere):**
```typescript
// Domain entity directly serialized to JSON and stored in DB
class User {
id: string
email: string
passwordHash: string // Exposed in API response!
createdAt: Date
lastLogin: Date
preferences: UserPreferences
roles: Role[]
toJSON() {
return { ...this } // Leaks everything
}
}
// Controller returns entity directly
app.get('/users/:id', (req, res) => {
const user = userRepo.findById(req.params.id)
res.json(user) // passwordHash in response!
})
// ORM couples domain to database schema
@Entity()
class User {
@PrimaryColumn() id: string
@Column() email: string
// Domain entity is now a database entity
}
```
**Correct (dedicated models per layer with mappers):**
```typescript
// domain/User.ts - Pure domain entity
class User {
constructor(
readonly id: UserId,
private email: Email,
private passwordHash: PasswordHash,
private roles: Set<Role>
) {}
hasRole(role: Role): boolean {
return this.roles.has(role)
}
}
// infrastructure/persistence/UserEntity.ts - Database model
interface UserRow {
id: string
email: string
password_hash: string
roles: string // JSON array in DB
created_at: string
updated_at: string
}
// infrastructure/persistence/UserMapper.ts
class UserMapper {
toDomain(row: UserRow): User {
return new User(
new UserId(row.id),
Email.create(row.email),
new PasswordHash(row.password_hash),
new Set(JSON.parse(row.roles))
)
}
toPersistence(user: User): UserRow {
return {
id: user.id.value,
email: user.email.value,
password_hash: user.passwordHash.value,
roles: JSON.stringify([...user.roles]),
updated_at: new Date().toISOString()
}
}
}
// interface/dto/UserResponse.ts - API response model
interface UserResponse {
id: string
email: string
roles: string[]
// No passwordHash!
}
// interface/mappers/UserResponseMapper.ts
class UserResponseMapper {
toResponse(user: User): UserResponse {
return {
id: user.id.value,
email: user.email.value,
roles: [...user.roles]
}
}
}
```
**Benefits:**
- Database schema changes don't affect domain
- API can evolve independently of domain model
- Sensitive data never accidentally exposed
Reference: [Data Mapper Pattern](https://martinfowler.com/eaaCatalog/dataMapper.html)