From 8e605772b76f3143e13d440eefda3e072c92d4bf Mon Sep 17 00:00:00 2001 From: "cb-bot[bot]" Date: Fri, 27 Mar 2026 10:33:44 +0000 Subject: [PATCH] docs(contextbot): update coding conventions Applied 2 convention(s) from recent PR reviews: - AGENTS.md (CREATE): Created primary instruction file with all project conventions. Covers architecture (MVVM+ folder structure, model vs class distinction, caseless enum services, enum choices pattern), dependency injection (init defaults, FileIO closure struct, async closure injection), Swift style (guard-let early returns), and testing (co-ship tests, mirror directory layout, camelCase naming, isolated UserDefaults). - CLAUDE.md (SYMLINK): Created CLAUDE.md as a relative symlink to AGENTS.md to maximize compatibility across agents (Claude Code, Codex, Cursor) while keeping a single source of truth. --- AGENTS.md | 189 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ CLAUDE.md | 1 + 2 files changed, 190 insertions(+) create mode 100644 AGENTS.md create mode 120000 CLAUDE.md diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..1752bbe --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,189 @@ +# Clearance — Coding Conventions + +## Project Overview + +Clearance is a macOS markdown viewer/editor built with Swift 6, SwiftUI, and AppKit. It uses XcodeGen (`project.yml`) to generate the Xcode project. The minimum deployment target is macOS 14. + +## Architecture + +### Directory Structure + +Organize source files into the MVVM+ folder structure under `Clearance/`: + +- `Models/` — Data types (structs) and observable state holders (classes) +- `Services/` — Stateless business logic and I/O abstractions +- `ViewModels/` — View-driving logic +- `Views/` — SwiftUI views (with subdirectories: `Edit/`, `Render/`, `Sidebar/`) +- `Windows/` — AppKit window controllers +- `App/` — App entry point and delegate + +Do not place source files flat in the `Clearance/` root. + +### Model vs Class Distinction + +Use plain structs for data-carrying model types (e.g. `RecentFileEntry`, `RemoteDocument`). Use `final class` conforming to `ObservableObject` with `@Published` properties for types that hold mutable state and drive UI updates (e.g. `RecentFilesStore`, `DocumentSession`, `AppSettings`). Mark `@Published` properties as `private(set)` when they should only be mutated internally. + +```swift +// Good: plain struct for data +struct RemoteDocument: Equatable { + let requestedURL: URL + let renderURL: URL +} + +// Good: final class for observable state +final class RecentFilesStore: ObservableObject { + @Published private(set) var entries: [RecentFileEntry] +} +``` + +### Stateless Services as Caseless Enums + +Implement pure service types that have no stored state as caseless enums with static methods. This prevents accidental instantiation and makes statelessness explicit. + +```swift +// Good +enum AddressBarFormatter { + static func displayText(for url: URL?) -> String { ... } +} + +// Bad +struct AddressBarFormatter { + func displayText(for url: URL?) -> String { ... } +} +``` + +### Enums with User-Facing Choices + +Enums representing user-facing choices must use `String` raw values and conform to `CaseIterable, Identifiable`. Add a computed `id` property (returning `rawValue` or `self`) and a computed `title` property with human-readable display text. + +```swift +enum WorkspaceMode: String, CaseIterable, Identifiable { + case view + case edit + + var id: String { rawValue } + var title: String { + switch self { + case .view: return "View" + case .edit: return "Edit" + } + } +} +``` + +## Dependency Injection & Testability + +### Init Parameters with Production Defaults + +Accept dependencies (UserDefaults, file I/O, network) as init parameters with default values pointing to production implementations. Do not hard-code dependencies inside initializers. + +```swift +// Good +init(userDefaults: UserDefaults = .standard, storageKey: String = "recentFiles", maxEntries: Int = 200) { + self.userDefaults = userDefaults +} + +// Bad +init() { + self.userDefaults = UserDefaults.standard +} +``` + +### FileIO Closure Struct Pattern + +Abstract file system operations through a struct with closure properties and a static `.live` instance. Do not use protocol-based mocking for I/O. + +```swift +struct FileIO: Sendable { + var read: @Sendable (URL) throws -> String + var write: @Sendable (String, URL) throws -> Void + + static let live = FileIO( + read: { url in try String(contentsOf: url, encoding: .utf8) }, + write: { text, url in try text.write(to: url, atomically: true, encoding: .utf8) } + ) +} +``` + +### Async Dependencies via Closures + +For async dependencies (e.g. network fetching), inject a closure typed as `@escaping @Sendable (URL) async throws -> T` with a default calling the production implementation. Do not create protocol + mock class pairs. + +```swift +// Good +init(remoteDocumentLoader: @escaping @Sendable (URL) async throws -> RemoteDocument = { url in + try await RemoteDocumentFetcher.fetch(url) +}) { ... } + +// Bad +protocol RemoteDocumentLoading { + func fetch(_ url: URL) async throws -> RemoteDocument +} +class MockRemoteDocumentLoader: RemoteDocumentLoading { ... } +``` + +## Swift Style + +### Guard-Let Early Returns + +Use `guard let` / `guard` for early returns rather than nesting logic inside `if let` blocks. Keep the main logic path at the top indentation level. + +```swift +// Good +static func allows(_ url: URL?) -> Bool { + guard let url else { return true } + guard let scheme = url.scheme?.lowercased() else { return true } + // main logic at top level +} + +// Bad +static func allows(_ url: URL?) -> Bool { + if let url = url { + if let scheme = url.scheme?.lowercased() { + // deeply nested + } + } + return true +} +``` + +## Testing + +### Co-ship Tests with New Code + +Every commit that introduces a new model or service must include corresponding unit tests in the same commit. + +### Test File Organization + +Test files live in `ClearanceTests/` mirroring the `Clearance/` source structure. Name each test file after the type it tests with a `Tests` suffix. + +``` +Clearance/Models/RecentFilesStore.swift → ClearanceTests/Models/RecentFilesStoreTests.swift +Clearance/Services/AddressBarFormatter.swift → ClearanceTests/Services/AddressBarFormatterTests.swift +``` + +### Test Method Naming + +Name test methods as descriptive camelCase sentences starting with `test` that describe the expected behavior. Do not use underscores or given/when/then structure. + +```swift +// Good +func testReopeningFileMovesItToTopWithoutDuplicates() { ... } +func testAutosaveWritesUpdatedContent() { ... } + +// Bad +func test_adding_file_places_at_top() { ... } +func testAdd() { ... } +func test_given_existing_entry_when_reopened_then_moves_to_top() { ... } +``` + +### Isolated UserDefaults in Tests + +Tests that exercise persistent storage must create isolated `UserDefaults` instances with unique suite names and call `removePersistentDomain` before use. Do not use `UserDefaults.standard` or protocol-based mocks in tests. + +```swift +let suite = "RecentFilesStoreTests-1" +let defaults = UserDefaults(suiteName: suite)! +defaults.removePersistentDomain(forName: suite) +let store = RecentFilesStore(userDefaults: defaults, storageKey: "recent") +``` diff --git a/CLAUDE.md b/CLAUDE.md new file mode 120000 index 0000000..47dc3e3 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1 @@ +AGENTS.md \ No newline at end of file