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.
106 lines
3.0 KiB
Markdown
106 lines
3.0 KiB
Markdown
---
|
|
title: Defer Framework and Database Decisions
|
|
impact: MEDIUM-HIGH
|
|
impactDescription: preserves optionality, reduces early commitment risk
|
|
tags: bound, deferral, decisions, optionality
|
|
---
|
|
|
|
## Defer Framework and Database Decisions
|
|
|
|
A good architecture allows major decisions about frameworks, databases, and delivery mechanisms to be deferred until the last responsible moment. The longer you wait, the more information you have.
|
|
|
|
**Incorrect (early commitment to specifics):**
|
|
|
|
```typescript
|
|
// Day 1: "Let's use Prisma with PostgreSQL and Next.js"
|
|
|
|
// domain/Order.ts - Coupled to Prisma from the start
|
|
import { PrismaClient } from '@prisma/client'
|
|
|
|
const prisma = new PrismaClient()
|
|
|
|
export async function createOrder(data: OrderInput) {
|
|
// Business logic intertwined with Prisma
|
|
const order = await prisma.order.create({
|
|
data: {
|
|
items: {
|
|
create: data.items.map(item => ({
|
|
productId: item.productId,
|
|
quantity: item.quantity,
|
|
price: item.price
|
|
}))
|
|
},
|
|
total: data.items.reduce((sum, i) => sum + i.price * i.quantity, 0)
|
|
},
|
|
include: { items: true }
|
|
})
|
|
|
|
return order
|
|
}
|
|
|
|
// 6 months later: "PostgreSQL doesn't scale for our read patterns,
|
|
// we need DynamoDB" - Massive rewrite required
|
|
```
|
|
|
|
**Correct (defer database decision):**
|
|
|
|
```typescript
|
|
// Day 1: Focus on business rules, defer database choice
|
|
|
|
// domain/Order.ts - Pure business logic
|
|
export class Order {
|
|
private items: OrderItem[] = []
|
|
|
|
addItem(product: Product, quantity: number): void {
|
|
if (quantity <= 0) throw new InvalidQuantityError()
|
|
this.items.push(new OrderItem(product, quantity))
|
|
}
|
|
|
|
calculateTotal(): Money {
|
|
return this.items.reduce(
|
|
(sum, item) => sum.add(item.lineTotal()),
|
|
Money.zero()
|
|
)
|
|
}
|
|
}
|
|
|
|
// application/ports/OrderRepository.ts - Interface only
|
|
export interface OrderRepository {
|
|
save(order: Order): Promise<void>
|
|
findById(id: OrderId): Promise<Order | null>
|
|
}
|
|
|
|
// For now: Simple in-memory implementation for testing
|
|
// infrastructure/InMemoryOrderRepository.ts
|
|
export class InMemoryOrderRepository implements OrderRepository {
|
|
private orders = new Map<string, Order>()
|
|
|
|
async save(order: Order): Promise<void> {
|
|
this.orders.set(order.id.value, order)
|
|
}
|
|
}
|
|
|
|
// Later, when you know more: Add real database
|
|
// infrastructure/PrismaOrderRepository.ts
|
|
// OR
|
|
// infrastructure/DynamoOrderRepository.ts
|
|
// OR
|
|
// infrastructure/MongoOrderRepository.ts
|
|
|
|
// The domain never changes regardless of database choice
|
|
```
|
|
|
|
**Decisions worth deferring:**
|
|
- Database vendor (PostgreSQL vs MongoDB vs DynamoDB)
|
|
- ORM choice (Prisma vs TypeORM vs raw SQL)
|
|
- Web framework (Express vs Fastify vs Hono)
|
|
- Message queue (RabbitMQ vs Kafka vs SQS)
|
|
- Cache provider (Redis vs Memcached)
|
|
|
|
**Benefits:**
|
|
- Learn requirements before committing
|
|
- Prototype faster with simple implementations
|
|
- Switch vendors without domain rewrites
|
|
|
|
Reference: [Clean Architecture - The Database is a Detail](https://www.oreilly.com/library/view/clean-architecture-a/9780134494272/ch30.xhtml)
|