Skip to content

Contributing

ChiFungHillmanChan edited this page Apr 19, 2026 · 2 revisions

Contributing

Branching

  • main is always green: swift test passes, xcodebuild succeeds.
  • Feature work lives on feat/<short-name> branches.
  • Bug fixes on fix/<issue-number>-<short-name>.
  • Docs on docs/<short-name>.
  • Open a PR against main. Squash-and-merge is fine for small changes; merge-commit is fine for multi-commit features.

Test-driven development

Every feature and bug fix follows TDD:

  1. Write the failing test first.
  2. Run swift test (or the relevant filter) and confirm it fails for the right reason.
  3. Write the minimum code to make it pass.
  4. Refactor if needed.
  5. Commit.

For code that can't be unit-tested (AX API, Carbon hotkeys, NSWindow lifecycle, EventKit trigger watchers, NSWorkspace app launching, Shortcuts URL scheme), manually verify with docs/TESTING.md (V0.1 / V0.2 / V0.3 / V0.4 smoke sections) and write a short note in the PR describing what you observed. V0.4's test suite is 157 pure-logic tests — add more when you add new SceneCore types. The V0.4 cycle followed TDD heavily for Workspace / WorkspaceStore / WorkspaceTrigger: write the Codable round-trip test first, make it pass, then move on.

Commit style

Use Conventional Commits:

  • feat(core): <what> — new behavior in SceneCore
  • feat(app): <what> — new behavior in SceneApp
  • fix(core|app): <what> — bug fix
  • chore: <what> — tooling, scaffolding, build changes
  • docs: <what> — README, wiki, install docs
  • test: <what> — new tests only (no production code change)

Commit messages should explain the why, not the what. The diff already shows the what.

Co-Authored-By trailer: the V0.4 cycle used Claude Code for large swaths of the implementation. When AI assisted materially with a commit, add the trailer at the bottom of the commit message:

Co-Authored-By: Claude <noreply@anthropic.com>

Keep the trailer honest — only add it when the AI actually contributed, not as a blanket policy. Pass the message via a HEREDOC so the trailer isn't mangled:

git commit -m "$(cat <<'EOF'
feat(core): LayoutEngine.apply retries Electron windows

Cursor / VS Code / Slack settle ±3–5 px off the target rect on the
first setFrame. Retry once with a compensating offset so the window
lands within tolerance. One retry is enough in practice; more tend to
oscillate without converging.

Co-Authored-By: Claude <noreply@anthropic.com>
EOF
)"

PR checklist

Before opening a PR:

  • swift test passes locally.
  • xcodebuild succeeds for the SceneApp scheme.
  • You ran relevant items from Testing if the change touches runtime behavior.
  • No new external dependencies added to Package.swift without discussion.
  • No files under docs/superpowers/, .claude/, CLAUDE.md, or other AI-session artifacts sneaked in (they should stay gitignored).
  • README.md + wiki pages updated if user-visible behavior changed.

What goes where

When adding new code, follow the layering:

  • Pure logic, no system calls? → Sources/SceneCore/Layout/ or Sources/SceneCore/Interaction/ or Sources/SceneCore/Workspace/. Write unit tests.
  • Wraps an AX / CoreGraphics / Carbon API? → Sources/SceneCore/AX/ or Sources/SceneCore/Interaction/. Compile-check only (AX API can't be mocked cleanly).
  • Touches NSWindow, NSEvent, MenuBarExtra, AppDelegate, NSWorkspace, EventKit, Shortcuts URL scheme? → SceneApp/SceneApp/. Verified via the smoke test.
  • Touches user-visible copy / onboarding? → add the key to SceneApp/SceneApp/Resources/Localizable.xcstrings (V0.4 switched fully to the String Catalog). Use LocalizedStringKey in SwiftUI (Text("settings.tab.workspaces")). At minimum, add en + zh-HK values; zh-TW + ja fall back to en if missing.

If a change crosses the SceneCore / SceneApp boundary, split into two commits — the first touches only SceneCore, the second wires it up in SceneApp.

Concurrency rules

Swift 6 strict concurrency is on. A few patterns that come up often:

  • UI-touching types (Coordinator, DragSwapController, OnboardingWindowController) are @MainActor.
  • Files declaring @Published or ObservableObject without importing SwiftUI must import Combine explicitly.
  • Delayed work started from a non-main actor closure (e.g., UNUserNotificationCenter.getNotificationSettings) must hop to DispatchQueue.main.async before updating NSStatusItem.
  • Use Task { @MainActor in … } inside NSEvent global monitors, Carbon event handlers, and timers.

No external dependencies

Keep Package.swift dependencies empty. The current tech stack (Foundation, AppKit, SwiftUI, Carbon, ApplicationServices, UserNotifications, EventKit, os.Logger) is enough through V0.4. Adding a dep should require a concrete rationale and reviewer sign-off.

Code style

  • Swift 5.9, 4-space indent, 120-col soft limit.
  • public only where the API truly needs to be re-exported; prefer internal.
  • No comments explaining what the code does — rely on clear names. Comments are only for why (a non-obvious invariant, a workaround for a specific macOS behavior).
  • Error types live in the module that throws them. Keep the cases small and specific.

Clone this wiki locally