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.
102 lines
3.1 KiB
Markdown
102 lines
3.1 KiB
Markdown
---
|
|
title: Use Cases Must Not Contain Presentation Logic
|
|
impact: HIGH
|
|
impactDescription: enables multiple UI implementations from same use case
|
|
tags: usecase, presentation, formatting, separation
|
|
---
|
|
|
|
## Use Cases Must Not Contain Presentation Logic
|
|
|
|
Use cases return domain data, not formatted strings, HTML, or UI-specific structures. Presentation logic belongs in the interface adapters layer.
|
|
|
|
**Incorrect (presentation logic in use case):**
|
|
|
|
```typescript
|
|
class GetUserProfileUseCase {
|
|
execute(userId: string): UserProfileResponse {
|
|
const user = this.repo.findById(userId)
|
|
const orders = this.orderRepo.findByUser(userId)
|
|
|
|
return {
|
|
displayName: `${user.firstName} ${user.lastName}`, // Formatting
|
|
memberSince: user.createdAt.toLocaleDateString('en-US', {
|
|
month: 'long',
|
|
year: 'numeric'
|
|
}), // Locale-specific formatting
|
|
avatarHtml: `<img src="${user.avatarUrl}" alt="${user.firstName}"/>`, // HTML!
|
|
orderSummary: orders.length > 0
|
|
? `${orders.length} orders totaling $${this.sumOrders(orders).toFixed(2)}`
|
|
: 'No orders yet', // UI text
|
|
recentOrders: orders.slice(0, 5).map(o => ({
|
|
...o,
|
|
statusBadgeColor: this.getStatusColor(o.status) // UI styling
|
|
}))
|
|
}
|
|
}
|
|
|
|
private getStatusColor(status: string): string {
|
|
const colors = { pending: 'yellow', shipped: 'blue', delivered: 'green' }
|
|
return colors[status] || 'gray'
|
|
}
|
|
}
|
|
```
|
|
|
|
**Correct (use case returns domain data):**
|
|
|
|
```typescript
|
|
// application/usecases/GetUserProfileUseCase.ts
|
|
class GetUserProfileUseCase {
|
|
execute(userId: string): UserProfileResult {
|
|
const user = this.repo.findById(userId)
|
|
const orders = this.orderRepo.findByUser(userId)
|
|
|
|
return {
|
|
user: {
|
|
id: user.id.value,
|
|
firstName: user.firstName,
|
|
lastName: user.lastName,
|
|
email: user.email.value,
|
|
avatarUrl: user.avatarUrl,
|
|
createdAt: user.createdAt
|
|
},
|
|
orders: orders.map(o => ({
|
|
id: o.id.value,
|
|
total: o.total.amount,
|
|
currency: o.total.currency,
|
|
status: o.status,
|
|
createdAt: o.createdAt
|
|
})),
|
|
orderCount: orders.length,
|
|
totalSpent: this.sumOrders(orders)
|
|
}
|
|
}
|
|
}
|
|
|
|
// interface_adapters/presenters/UserProfilePresenter.ts
|
|
class UserProfilePresenter {
|
|
present(result: UserProfileResult, locale: string): UserProfileViewModel {
|
|
const formatter = new Intl.DateTimeFormat(locale, {
|
|
month: 'long',
|
|
year: 'numeric'
|
|
})
|
|
|
|
return {
|
|
displayName: `${result.user.firstName} ${result.user.lastName}`,
|
|
memberSince: formatter.format(result.user.createdAt),
|
|
avatarUrl: result.user.avatarUrl,
|
|
orderSummary: this.formatOrderSummary(result, locale),
|
|
recentOrders: result.orders.slice(0, 5).map(o =>
|
|
this.formatOrder(o, locale)
|
|
)
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
**Benefits:**
|
|
- Same use case serves web, mobile, API consumers
|
|
- Locale/format changes don't touch business logic
|
|
- UI redesigns don't affect use case tests
|
|
|
|
Reference: [Clean Architecture - Presenters](https://www.oreilly.com/library/view/clean-architecture-a/9780134494272/ch23.xhtml)
|