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.
120 lines
2.7 KiB
Markdown
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)
|