Coding conventions for BrewUI — BrewUI-specific naming, habits, and pointers. Does not repeat stack, layers, constraints, or folder layout; see
ARCHITECTURE.md(Tech Stack, Constraints & decisions, File organisation). Expand this file as real code appears. Durable why lives in.ai/memory.md.
Stack, targets, and product constraints: ARCHITECTURE.md — Tech Stack and Constraints & decisions.
Follow Swift API Design Guidelines and project SwiftLint / SwiftFormat config (.swiftlint.yml, .swiftformat).
Layer naming (roles in ARCHITECTURE.md — Core components):
- Repositories: protocol
FormulaRepository; concreteBrewFormulaRepository; testsMockFormulaRepository. Installed inventory:InstalledPackagesRepository/BrewInstalledPackagesRepository(see Testing for boundary fakes). - Services: subprocess execution via protocol
BrewCommandRunningandBrewCommandService; resolvebrewwithBrewExecutableLocatorconforming toBrewExecutableLocating(default prefix order matches product constraints inARCHITECTURE.md). - Interactors: protocol
…Interacting; type…Interactor; mockMock…Interactor. Skip aBrewprefix when the name is already clear. - ViewModels: named for screen or tab (e.g.
InstalledViewModel).
- Feature layout: Each feature folder uses
Views/andViewModels/. Place feature*Itemtypes and viewmodel-local presentation helpers inViewModels/. - Domain models only:
Models/is for domain value types/relationships only. Do not place UI/presentation helpers, command/API transport payloads, or infrastructure cache snapshots there. - UI-related types: Put feature-specific UI/presentation types in the relevant feature folder (
Features/<Feature>/ViewsorFeatures/<Feature>/ViewModels), not inModels/. - Brew command transport/state: Keep brew command execution, command-center types, command operation models, and
brew infocommand JSON decoding underServices/BrewCommand/. - Networking transport: Keep API client and its transport payloads/errors grouped together under a dedicated services networking boundary (for example
Services/API/when introduced). - DB transport: Keep DB models/mappers alongside DB access layer types under one DB boundary folder (for example
Services/Database/orRepositories/Database/), not inModels/.
- Formatting / lint:
.swiftformat,.swiftlint.ymlare authoritative for mechanical rules. - Concurrency: do not turn off strict concurrency; protect shared mutable state (
@MainActor, actors,Sendable). - Paths: no hard-coded install paths — locate
brewviaProcessInfo/FileManager(seeAGENTS.md). - Otherwise prefer idiomatic Swift; use Apple’s language and SwiftUI docs for general patterns.
UI in Brew/ uses semantic tokens under Brew/Theme/ (BrewColors, BrewSpacing / BrewLayout / BrewRadius, BrewFonts). Do not hard-code colours, spacing, or typography in feature views — add or extend tokens in Theme when new semantics appear. Cursor agents: see .cursor/rules/design-system.mdc.
Errors: Prefer typed Error enums with associated values where useful. Separate user-facing copy from technical detail; log or preserve detail; do not swallow errors silently.
SwiftUI: See SwiftUI documentation. UI work on @MainActor; prefer .task over .onAppear for async work tied to view lifetime.
Repository boundary: Repositories are data-access adapters. They fetch/parse/map source data and expose that data to the app, but they do not invent extra informational or presentation values beyond what the source provides. Any hardcoded informational text, user guidance, or display-only derived strings belong in ViewModel/View layers (or another presentation-oriented layer), not repositories.
Transport boundary: Do not expose Codable transport payload types (*JSON, DTOs, wire models) from service or repository APIs. Decode transport payloads at the boundary, then map to app-facing domain models (Models/) or explicit feature-layer data contracts before returning.
Command transparency: When the UI exposes a Homebrew command, render a copyable, user-facing command that a person can run in Terminal (for example, brew info <name>). Do not display internal implementation flags used only for app parsing/workflow (for example, --json=v2) in user-visible command text.
MVVM boundary: Keep views as passive as practical. Put view-facing UI policy, derived flags, and decision/branching logic in ViewModels (for example, split/detail visibility booleans and action-routing decisions). Views should primarily bind/render and forward actions. Keep view-layer branching limited to simple presentation branches (for example, loading/error/empty content blocks) and avoid embedding cross-state decision trees in views. Add unit tests for non-trivial ViewModel-derived UI state.
Passive view enforcement: A view must not compose multiple ViewModel booleans (or other raw state primitives) inline to derive a single UI concern. If a UI element has one presentation state (for example, showing one spinner), expose one ViewModel property for that state and bind directly to it.
ViewModel itemization: When a ViewModel grows with mapped presentation fields that broadly change together, extract that co-changing mapping into feature-layer *Item types and expose those items from the ViewModel for subviews to consume. Keep independently-changing async stream state (for example, isUpgrading / isUninstalling) on the top-level ViewModel.
Domain-to-presentation mapping boundary: Do not add UI-facing presentation properties/extensions directly on domain model types. Map domain models into presentation in one of two places only: (1) feature ViewModels for top-level surfaces, or (2) feature *Item types for subview/action-specific presentation data.
Canonical package identity: HomebrewPackageID (Brew/Models/HomebrewPackageReference.swift) is the single canonical identity type for every Homebrew package across the app. Any domain model, list item, or value that identifies — or is directly backed by — a package must type that identity as HomebrewPackageID, never a bare String name/token or an ad-hoc id type. Identifiable.id on package-backed types must be a HomebrewPackageID (the type is its own ID). Use its cases for stable formula/cask lookup (.formula(name:) for formulae, .cask(token:) for casks), and construct from raw String names only at decode/transport boundaries.
Root view dependency ownership: When a feature defines a *Root view wrapper, the root is the dependency-composition boundary for that surface. Root views must read app-level dependencies (for example @Environment values), construct and inject content-view dependencies, and own view-model lifecycle boundaries. Content views must focus on rendering and behavior and must not acquire those app-level dependencies directly when a root exists.
Anti-pattern to avoid: Do not make a content view model optional only to work around dependency acquisition inside the content view. Keep dependency resolution in the root and inject non-optional dependencies into the content view.
URL presentation: Any user-facing web URL (http/https) shown in the UI should be rendered as a tappable Link that opens the default browser, while still showing the literal URL text for transparency.
Loadable UI state: For screens/panels that are expected to load asynchronously and can fail, model presentation state as a single enum on the ViewModel (for example: .loading, .loaded(Data), .error(String)) instead of separate isLoading/data/error fields. This keeps states mutually exclusive, reduces invalid combinations, and gives views a single switch-based rendering path.
Previews: Use centralized preview data/mocks from Brew/PreviewSupport/AppPreviewSupport.swift; do not define one-off inline mock repositories/services in preview blocks. Add new preview sample data and lightweight preview fakes to that file so it remains the single source of truth.
Preview placement: Keep each view’s #Preview blocks at the bottom of the same file as that view, not in standalone +Previews.swift files.
Documentation: Use DocC / Xcode doc comments for non-obvious public / internal API. Inline // explains why, not what.
Accessibility: Meaningful labels (and hints where needed) on interactive controls; keyboard shortcuts where it matters. UI test IDs: the AXID enum in Sources/BrewAccessibilityID/, linked by both the app and BrewUITests. Attach it with .axid(_:); never write a raw identifier string in a view or a test. Identity is orthogonal to labels — keep accessibilityLabel for VoiceOver.
Testing: Prefer Swift Testing; XCTest is fine. Never invoke real brew in tests — mock/stub only boundaries: BrewCommandRunning (subprocess) and, when needed, BrewExecutableLocating (e.g. MissingBrewExecutableLocator for “brew not found”). Prefer slice tests that use the real BrewInstalledPackagesRepository (and thus real parsing) with those fakes; shared helpers live under BrewTests/TestSupport/. Pure presentation tests may use InstalledViewModel’s init(testing…) without a repository. Cover errors and async paths, not only happy paths. See ARCHITECTURE.md for layer flow.
Test shape: Prefer one logical behavior per @Test — typically a single #expect, or one equality check on a small Equatable snapshot (e.g. expected rows + errors + flags) so related outcomes stay one assertion. BrewInstalledPackagesRepository.live() is not a unit-test target: it wires real BrewCommandService and filesystem discovery; rely on slice tests with fakes and UI/manual smoke if needed.
- Prefer Foundation / SwiftUI. Add packages sparingly; justify in
Package.swiftand lock versions.
- Imperative subject lines (Add …, not Added …). One logical change per commit when practical.
Add patterns when they stabilize in code. Cross-cutting decisions go in .ai/memory.md.