Guide for implementing features in ClaudeBar following architecture-first design, TDD, rich domain models, and Swift 6.2 patterns...
Implement features using architecture-first design, TDD, rich domain models, and Swift 6.2 patterns.
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β 1. ARCHITECTURE DESIGN (Required - User Approval Needed) β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ€
β β’ Read the design docs β they are the source of truth β
β β’ Place the feature: owner, capability, extension point β
β β’ Write the design change into the docs first β
β β’ Analyze requirements β
β β’ Create component diagram β
β β’ Show data flow and interactions β
β β’ Present to user for review β
β β’ Wait for approval before proceeding β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β
βΌ (User Approves)
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β 2. TDD IMPLEMENTATION β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ€
β β’ Model tests β models (Quotas / Providers / Domain) β
β β’ Module tests β generic rules and workers (DataSources) β
β β’ Views, reading the domain directly β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
Before writing any code, design it in the docs and get user approval. The design docs are the source of truth (AGENTS.md β Design docs are the source of truth).
Read the design in order (ARCHITECTURE.md maps it):
USER_JOURNEYS.md (the person and the moment),
CANONICAL_MODEL.md (the tree, the words, each law
and its owner, owned vs offered), TARGET_ARCHITECTURE.md (the
pieces and their one job, the flows), then any docs/features/<context>/<x>/design.md it touches. Then answer:
| Question | If yes |
|---|---|
| Does it answer another question than the product's lifecycle? | it is a capability (CANONICAL Β§2.1): declared in the definition, its own types, reached through a handle that is nil when not declared β never a flag on Provider, never chosen by a provider's name. A runner outside the modules is supplied by the Engine (TARGET_ARCHITECTURE Β§10) |
| Does a provider need something the definition can't say? | a generic rule or worker in DataSources (ENGINE_DESIGN), then a line of JSON β never a Swift line that names the provider |
| Does it react to a refresh, a selection, a setting? | it observes an extension point (e.g. QuotaMonitor.onRefreshed) β the Monitor is not edited for it |
| Does it notify? | its own port in Alerting β not a new method on an existing alerter |
| Does something already decide it? (a threshold crossed, a rate, a unit, the worst quota) | use that owner: the feature is a new destination or a new read of it, never a parallel engine β A new feature next to an old one |
| Does it ask a type a new kind of question? | a new value built from that type (ModelMix(of: days, in: unit)), not another read on it β question 6 |
| Does it save a choice, or a secret? | settings.json through JSONSettingsRepository; a secret through the one Keychain-with-fallback store (settings.md) β never a feature's own file |
| Does another open PR touch the same type? | agree on one owner first; the later one rebases on it |
| Is a law missing, or owned twice? | add it to CANONICAL Β§5 with one owner |
When the docs don't cover it, write the change into them first (tree lines, laws with owners, a pieces table, a TARGET section) β that doc change is the architecture you present in Step 4. Code that disagrees with the docs is behind; don't bend the design to it.
Before components, ask how the domain should be: from the root problem (the issue, the journey) to the facts it must respect, to the concepts those force, then compare with the code. Ask the seven questions of every type the feature touches, and draw now beside proposed β domain-shape.md. For each new type, write down its one reason to change and which side it lives on (kernel fact, App words or colours, Infrastructure storage, an OS port); a type with two reasons is two types. A new aggregate, a moved law or a changed owner goes into the docs in Step 0's change.
Identify:
Use ASCII diagram showing all components and their interactions:
Example: added accounts β a definition change plus generic pieces
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β ARCHITECTURE β
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ€
β Resources (data) Modules (Swift, no vendor names) App β
β β
β ββββββββββββββββ βββββββββββββββββββββββββββββββ βββββββββββββ β
β β codex.json ββββΆβ Providers ββββΆβ Accounts β β
β β accounts: β β ProviderCatalog.detect() β β card β β
β β folder, ids β β ProviderDefinition.Accountsβ βββββββββββββ β
β β dataSources β β Provider owns its Accounts β β
β ββββββββββββββββ ββββββββββββββββ¬βββββββββββββββ β
β β β
β ββββββββββββββββΌβββββββββββββββ β
β β DataSources β β
β β identity, requiresFiles, β β
β β JSON-RPC `then`, env β β
β ββββββββββββββββ¬βββββββββββββββ β
β βΌ β
β βββββββββββββββββββββββββββββββ β
β β Quotas (UsageSnapshot β¦) β β
β βββββββββββββββββββββββββββββββ β
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
List each component with:
Example:
| Component | Purpose | Inputs | Outputs | Dependencies |
|----------------|------------------------|-----------------|----------------|-----------------|
| AddedAccounts | Validate a login folder| folder path | account config | DataSources |
| identity rule | Fail closed on swaps | credential | UsageError | β |
IMPORTANT: Always ask user to confirm the design is correct before implementing β
the doc change (tree, laws and owners, pieces), the diagram, and for UI a mockup in
design-concept/<feature>/ on mock data. A new card, pane or screen has its
mockup before any view code; the review looks at the mockup first, then the domain.
Use AskUserQuestion tool with options:
Do NOT proceed to Phase 1 until user explicitly approves.
Domain models encapsulate behavior, not just data:
// Rich domain model with behavior
public struct UsageQuota: Sendable, Equatable {
public let percentRemaining: Double
// Domain behavior - computed from state
public var status: QuotaStatus {
QuotaStatus.from(percentRemaining: percentRemaining)
}
public var isDepleted: Bool { percentRemaining <= 0 }
public var needsAttention: Bool { status.needsAttention }
}
Views consume domain models directly from QuotaMonitor:
// QuotaMonitor is the single source of truth
@MainActor @Observable
public final class QuotaMonitor {
// The providers you keep: add, delete, order, the derived lineup
public let providers: Providers
public var logins: [Account] // every login
public var lineup: [Account] // the lineup
public func login(id: String) -> Account?
// Selection state
public var selectedProviderId: String
public var selectedLogin: Account?
public var selectedProviderStatus: QuotaStatus
}
// Views consume domain directly - NO AppState layer
struct MenuContentView: View {
let monitor: QuotaMonitor // Injected from app
var body: some View {
// Use delegation methods, not monitor.providers.enabled
ForEach(monitor.lineup, id: \.id) { login in
ProviderPill(provider: login)
}
}
}
Ports for what lies outside the app live at a module's root and are
@Mockable; their implementations are internal in Internal/:
@Mockable
public protocol NetworkClient: Sendable {
func request(_ request: URLRequest) async throws -> (Data, URLResponse)
}
Reference: MODULAR_DESIGN.md (modules) Β· TARGET_ARCHITECTURE.md (how a provider runs) Β· ARCHITECTURE.md (the app layers)
The code is mid-migration from three layers to modules. Find which side the behaviour lives on before you change it:
| Where | Holds | Tests |
|---|---|---|
Modules/Providers/Resources/Providers/<id>.json (+ .js) |
every built-in provider: where the key is, how to fetch, how to read | Modules/Providers/Tests/ (golden tests over StubbedProvider / ClaudeHarness) |
Modules/Providers/Sources |
ProviderCatalog (finds every definition), Engine (the shared ports), Provider (the one lifecycle: refresh, fallback chain, accounts), ProviderDefinition, settings and account contracts, the capabilities (UsageHistory, GuestPasses, InUse) |
Modules/Providers/Tests/ |
Modules/DataSources/Sources |
DataSource and its workers: credential lookups (environment, login shell, files, Keychain, browser cookies and storage, SQLite) and refreshes; HTTP, steps, JSON-RPC, terminal, command, file, directory, SQLite, local-server and CloudWatch fetches; JSON / text / script mapping; UsageLog and its log readers; the process runners |
Modules/DataSources/Tests/ |
Modules/Quotas/Sources |
the usage model: UsageSnapshot, UsageQuota, UsageError, plans, costs, DailyUsageStat / DailyUsageReport (interim shapes, see each type's - Note:) |
Tests/DomainTests/ and the tests of the module that uses it |
Modules/AWSClients |
the AWS SDK behind DataSources' CloudWatchClient and PriceCatalog ports |
Modules/AWSClients/Tests/ |
Sources/Domain |
QuotaMonitor, Notify!, sessions, In use's NewSessions |
Tests/DomainTests/ |
Sources/Infrastructure |
storage, notifications, hooks, the shell environment, Claude's guest-pass runner | Tests/InfrastructureTests/ |
Sources/App |
SwiftUI views reading the domain directly; ClaudeBarApp.init, the composition root, which builds the Engine and names no provider |
Tests/AppTests/, Tests/AcceptanceTests/ |
Inside Sources/Domain, Infrastructure, App and their tests, put a new file in the folder of the context that owns its question (CANONICAL_MODEL Β§7): a session rule in Activity/, a destination in Alerting/, a refresh rule in Monitoring/. The feature's docs go in docs/features/<context>/<x>/. A file that fits no context's question is a sign the design is missing a word; ask before inventing a folder.
A bug in a migrated provider is fixed in its JSON, or generically in
DataSources, never with vendor-named Swift. Modules never import Domain.
Key patterns:
Internal/, one factory enum per module (DataSources.make, ProviderFactory.make)@Mockable ports; Chicago-school tests assert on stateQuotaMonitor and Provider directlydataSourceKind, isOn) before a new sub-protocolName each test should <outcome> [when <situation>], in the person's words, never a method, type or mechanism verb β Naming tests.
We follow Chicago school TDD (state-based testing):
Test state and computed properties:
@Suite
struct FeatureModelTests {
@Test func `should be normal when half is left`() {
// Given - set up initial state
let model = FeatureModel(value: 50)
// When/Then - verify state/return value
#expect(model.status == .normal)
}
@Test func `should have 70 left and stay healthy after using 30 of 100`() {
// Given
var model = FeatureModel(value: 100)
// When - perform action
model.consume(30)
// Then - verify new state
#expect(model.value == 70)
#expect(model.status == .healthy)
}
}
Stub dependencies to return data, assert on resulting state:
@Suite
struct FeatureServiceTests {
@Test func `should load three items when the service answers`() async throws {
// Given - stub dependency to return data (not verify calls)
let mockClient = MockNetworkClient()
given(mockClient).fetch(any()).willReturn(validResponseData)
let service = FeatureService(client: mockClient)
// When
let result = try await service.fetch()
// Then - verify returned state, not interactions
#expect(result.items.count == 3)
#expect(result.status == .loaded)
}
}
Create the views. ClaudeBarApp.init is the composition root: it builds the
Engine (the ports every provider shares) and calls ProviderCatalog().detect().
A provider is never wired there; a new port is a new Engine field, built
once in init (keep startup work there, never in ClaudeBarMain). Acceptance
specs in Tests/AcceptanceTests/ compose real modules with stubbed ports.
tuist test Providers # one module's tests while working (schemes: Providers, DataSources, Domain, Infrastructure, AppTests, AcceptanceTests)
# tuist caches results; the final check, and a forced re-run of one suite, use xcodebuild:
xcodebuild test -workspace ClaudeBar.xcworkspace -scheme ClaudeBar-Workspace -destination 'platform=macOS,arch=arm64'
xcodebuild test -workspace ClaudeBar.xcworkspace -scheme ClaudeBar-Workspace \
-destination 'platform=macOS,arch=arm64' -only-testing:ProvidersTests/ClaudeAPITests
nil when not declared), extension point@Mockable for external dependenciesInternal/QuotaCardText, ModelColors); the kernel answers facts and formats only the amounts it owns, through the shared formattersettings.json; secrets in the one Keychain-with-fallback storeinternal; no Bool flag the caller decides; each aggregate changes only its own statescripts/demo-screenshots.sh) for UI changesxcodebuild test green (tuist test skips cached targets)