Skills move to skills/<name>/SKILL.md and the repo gains both manifests, so it is now a marketplace hosting exactly one plugin whose root is the repo root (source: "."). Claude Code installs it from git; no symlinks involved. Why: install.sh wired the library into every consuming repo as an ABSOLUTE symlink to $CHECKOUT_DIR. The checkout moved three times, most recently when local-dev@1966d5a dropped the embedded .skills/ copy, and nothing migrated the links — 243 dangling links across 12 repos, hidden from git by the **/.claude/skills/ ignore rule. Worse, ~/dev spans two path roots (/Users/mathias on the iCloud-shared Macs, /home/mathias on koala), so an absolute link stored there is broken on one side by construction. Re-running the installer cannot fix that; only removing the mechanism can. install.sh keeps the harnesses with no plugin system and drops from 42 links per harness to one directory link, via the convergent ~/.agents/skills convention that Pi and OpenCode both read: ~/.agents/skills -> $CHECKOUT_DIR/skills ~/.config/crush/skills -> ~/.agents/skills ~/.config/opencode/skills -> ~/.agents/skills wire_claude_repo is deleted outright rather than gated behind a flag — on a mixed-root fleet it cannot be made correct. Antigravity and Vibe targets go too; both unused. CHECKOUT_DIR keeps its host-local default deliberately: pointing it into ~/dev would put a git working tree inside iCloud with two concurrent writers. Taskfile drops five near-identical wiring loops (they had already drifted) and delegates to install.sh. `task check` gains frontmatter validation, a stray-root-entry gate, and manifest/tag version parity — the loose secret-hygiene.md sat unloadable for months because nothing checked layout. Verified: claude plugin validate . passes clean; a probe skill loaded as skills:zzprobe through --plugin-dir; task check green at 42 skills. Refs: #23 Bump-Type: minor Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BeAp5LTscnz2W7Ubt4eJ6m
8.5 KiB
Software Architecture
The Goal of Architecture
Enable the development team to:
- Add features with minimal friction
- Change existing features safely
- Remove features cleanly
- Test features in isolation
- Deploy independently when possible
Architectural Principles
1. Vertical Boundaries (Features/Slices)
Organize by feature, not by technical layer.
BAD: Layer-first
src/
controllers/
UserController.ts
OrderController.ts
services/
UserService.ts
OrderService.ts
repositories/
UserRepository.ts
OrderRepository.ts
GOOD: Feature-first
src/
users/
UserController.ts
UserService.ts
UserRepository.ts
orders/
OrderController.ts
OrderService.ts
OrderRepository.ts
Why: Changes to "users" feature stay in users/. High cohesion within features.
2. Horizontal Boundaries (Layers)
Separate concerns into layers with clear dependencies.
┌──────────────────────────────────────┐
│ Presentation │ UI, Controllers, CLI
├──────────────────────────────────────┤
│ Application │ Use Cases, Orchestration
├──────────────────────────────────────┤
│ Domain │ Business Logic, Entities
├──────────────────────────────────────┤
│ Infrastructure │ Database, APIs, External
└──────────────────────────────────────┘
3. The Dependency Rule
Dependencies point INWARD.
Infrastructure → Application → Domain
↓ ↓ ↓
(outer) (middle) (inner)
- Inner layers know NOTHING about outer layers
- Domain has zero dependencies on infrastructure
- Use interfaces to invert dependencies
// Domain defines the interface (inner)
interface UserRepository {
save(user: User): Promise<void>;
findById(id: UserId): Promise<User | null>;
}
// Infrastructure implements it (outer)
class PostgresUserRepository implements UserRepository {
save(user: User): Promise<void> {
// SQL here
}
}
// Domain service uses the interface
class UserService {
constructor(private repo: UserRepository) {} // Depends on abstraction
}
4. Contracts
Interfaces define boundaries between components.
// The contract
interface PaymentGateway {
charge(amount: Money, card: CardDetails): Promise<ChargeResult>;
refund(chargeId: string): Promise<RefundResult>;
}
// Multiple implementations possible
class StripeGateway implements PaymentGateway { }
class PayPalGateway implements PaymentGateway { }
class MockGateway implements PaymentGateway { } // For tests
5. Cross-Cutting Concerns
Concerns that span multiple features: logging, auth, validation, error handling.
Options:
- Middleware/interceptors
- Decorators
- Aspect-oriented approaches
- Base classes (use sparingly)
// Middleware approach
class LoggingMiddleware {
handle(request: Request, next: Handler): Response {
console.log(`Request: ${request.path}`);
const response = next(request);
console.log(`Response: ${response.status}`);
return response;
}
}
6. Conway's Law
"Organizations design systems that mirror their communication structure."
Implication: Team structure affects architecture. Align both intentionally.
Common Architectural Styles
Layered Architecture
Traditional layers: Presentation → Business → Persistence
Pros: Simple, well-understood Cons: Can become a "big ball of mud" without discipline
Hexagonal Architecture (Ports & Adapters)
Domain at center, adapters around the edges.
┌─────────────────────┐
│ HTTP Adapter │
└─────────┬───────────┘
│
┌─────────────────▼─────────────────┐
│ DOMAIN │
│ ┌─────────────────────────┐ │
│ │ Business Logic │ │
│ │ Use Cases │ │
│ └─────────────────────────┘ │
└─────────────────┬─────────────────┘
│
┌─────────▼───────────┐
│ Database Adapter │
└─────────────────────┘
Ports: Interfaces defined by the domain Adapters: Implementations that connect to the outside world
Clean Architecture
Similar to Hexagonal, with explicit layers:
- Entities - Enterprise business rules
- Use Cases - Application business rules
- Interface Adapters - Controllers, Presenters, Gateways
- Frameworks & Drivers - Web, DB, External interfaces
Feature-Driven Structure (Frontend)
src/
features/
auth/
components/
LoginForm.tsx
SignupForm.tsx
hooks/
useAuth.ts
services/
authService.ts
types/
auth.types.ts
index.ts # Public API
checkout/
components/
hooks/
services/
types/
index.ts
shared/
components/ # Truly shared UI
hooks/ # Truly shared hooks
utils/ # Truly shared utilities
Feature-Driven Structure (Backend)
src/
modules/
users/
domain/
User.ts
UserRepository.ts # Interface
application/
CreateUser.ts # Use case
GetUser.ts # Use case
infrastructure/
PostgresUserRepo.ts
presentation/
UserController.ts
UserDTO.ts
orders/
domain/
application/
infrastructure/
presentation/
shared/
domain/ # Shared value objects
infrastructure/ # Shared infra utilities
The Walking Skeleton
Start with a minimal end-to-end slice:
- Thinnest possible feature that touches all layers
- Deployable from day one
- Proves the architecture works
Example walking skeleton for e-commerce:
- User can view ONE product (hardcoded)
- User can add it to cart
- User can "checkout" (just logs)
From there, flesh out each feature fully.
Testing Architecture
┌────────────────────────────────────────────┐
│ E2E / Acceptance Tests │ Few, slow, high confidence
├────────────────────────────────────────────┤
│ Integration Tests │ Some, medium speed
├────────────────────────────────────────────┤
│ Unit Tests │ Many, fast, isolated
└────────────────────────────────────────────┘
Test by layer:
- Domain: Unit tests (most tests here)
- Application: Integration tests with mocked infra
- Infrastructure: Integration tests with real dependencies
- E2E: Critical paths only
Architecture Decision Records (ADRs)
Document significant decisions:
# ADR 001: Use PostgreSQL for persistence
## Status
Accepted
## Context
We need a database. Options: PostgreSQL, MongoDB, MySQL
## Decision
PostgreSQL for:
- ACID compliance
- Team familiarity
- JSON support for flexibility
## Consequences
- Need PostgreSQL expertise
- Schema migrations required
- Excellent query capabilities
Red Flags in Architecture
- Circular dependencies between modules
- Domain depending on infrastructure
- Framework code in business logic
- No clear boundaries between features
- Shared mutable state across modules
- "Util" or "Common" packages that grow forever
- Database schema driving domain model