Last updated: 2026-03-24
- App:
Pindrop(menu bar macOS app,LSUIElementbehavior) - Stack: Swift 5.9+, SwiftUI, SwiftData, Swift Testing, XCTest UI tests
- Platform target: macOS 14+
- Main dependency path:
Pindrop.xcodeproj+ SwiftPM - Entry points:
Pindrop/PindropApp.swift,Pindrop/AppCoordinator.swift
- App code:
Pindrop/ - Services:
Pindrop/Services/ - UI:
Pindrop/UI/ - Persistence models:
Pindrop/Models/ - Utilities/logging:
Pindrop/Utils/ - Tests:
PindropTests/ - Test doubles:
PindropTests/TestHelpers/ - Build automation:
justfile,scripts/,.github/workflows/
- Xcode with command-line tools (
xcodebuild) justfor all routine workflows:brew install just- Optional:
swiftlint,swiftformat,create-dmg - Apple Developer signing configured in Xcode for signed local/release builds; CI recipes use explicit unsigned overrides
Prefer just recipes over ad-hoc shell commands.
just build # Debug build (ALWAYS use this when testing builds)
just build-release # Release build
just export-app # Developer ID export for distribution
just dmg # Signed DMG for distribution
just test # Unit test plan
just test-integration # Integration test plan (opt-in)
just test-ui # UI test plan
just test-all # Unit + integration + UI
just test-coverage # Unit tests with coverage
just dev # clean + build + test
just ci # clean + unsigned build + unsigned test + unsigned release build
just run # open Xcode project
just xcode # open Xcode projectDirect focused test commands:
xcodebuild test -project Pindrop.xcodeproj -scheme Pindrop -testPlan Unit -destination 'platform=macOS'
xcodebuild test -project Pindrop.xcodeproj -scheme Pindrop -testPlan UI -destination 'platform=macOS'
xcodebuild test -project Pindrop.xcodeproj -scheme Pindrop -destination 'platform=macOS' -only-testing:PindropTests/AudioRecorderTests
xcodebuild test -project Pindrop.xcodeproj -scheme Pindrop -destination 'platform=macOS' -only-testing:PindropTests/AudioRecorderTests/testStartRecordingRequestsPermission- Follow existing file header style (
Created on YYYY-MM-DD) - Use
final classfor services and most concrete implementations - Actor isolation pattern: services are usually
@MainActor - Known exception: hotkey internals with Carbon/event constraints
- Use
@Observablefor reactive services where compatible SettingsStoreintentionally usesObservableObject+@AppStorage- Keep import groups consistent with existing files
- Dependency injection via initializer arguments (avoid hidden globals)
- Protocol abstractions for hardware/system boundaries
- Example protocol seam:
AudioCaptureBackendinPindrop/Services/AudioRecorder.swift - Keep async boundaries explicit (
async/async throws) - Avoid fire-and-forget tasks unless they are UI/lifecycle orchestration
- Define domain errors as
enum ...: Error, LocalizedError - Keep user-facing messaging in
errorDescription - Catch at boundaries, log with context, then rethrow typed errors when possible
- Do not swallow errors with empty catch blocks
- String Catalogs:
Pindrop/Localization/Localizable.xcstrings(in-app copy) andPindrop/Localization/InfoPlist.xcstrings(privacy strings, bundle display name). Both are in the app target’s Copy Bundle Resources. The top-levelLocalization/tree is the source of truth for the YAML-first pipeline. - Runtime API:
localized("English key", locale: locale)inPindrop/AppLocalization.swiftnow resolves through generated stable-key metadata before falling back toBundle;SettingsStore.selectedAppLocaledrives UI locale andSettingsStore.selectedAppLanguagedrives dictation/transcription language. - New user-facing strings: Add an entry to the YAML source tree under
Localization/, then runjust l10n-syncso the catalogs and generated Swift stay in sync. - New language (locale): Add the locale with
just l10n-add-locale <locale>(or editLocalization/locales.yml), then populate the relevantLocalization/app/*.ymlandLocalization/infoplist/*.ymlfiles before syncing. - Interface vs dictation language: The General settings UI now separates interface language from dictation language. Keep
AppLocale-driven UI locale changes away fromAppLanguage/transcription behavior. - AI enhancement prompts:
AIEnhancementSettingsViewlocalizes default prompts for display; if the user saves without editing, the localized prompt text can be persisted and sent to the API—expect models to follow non-English system prompts, or keep defaults in English if you change that flow. - Localization tooling: Use
just l10n-import-current,just l10n-sync, andjust l10n-lint. The oldscripts/translate_xcstrings.pyhelper is obsolete.
- Use
Logcategories fromPindrop/Utils/Logger.swift - Categories include:
audio,transcription,model,output,hotkey,app,ui,update,aiEnhancement,context - Log intent and failure context; avoid noisy per-frame spam
- Models use SwiftData macros (
@Model,@Attribute(.unique)) - Keep schema-related changes coordinated with schema files under
Pindrop/Models/ - Use in-memory model containers for unit tests when testing store logic
- Test files:
*Tests.swift - Unit tests use Swift Testing with
@Suite/@Test; macOS UI coverage stays inPindropUITests/with XCTest UI APIs - Standard naming:
sutfor system under test - Prefer local fixture builders over shared
setUp/tearDown; usePindropTests/TestSupport.swiftfor reusable test helpers - Use protocol mocks from
PindropTests/TestHelpers/for hardware/system APIs - Integration tests are gated (see
PINDROP_RUN_INTEGRATION_TESTSpattern) - Test mode signal exists in runtime (
PINDROP_TEST_MODE) - UI tests run through
PINDROP_UI_TEST_MODEand deterministic fixture surfaces inPindrop/AppTestMode.swift
- Keep fixes minimal and local; do not refactor unrelated code in bugfixes
- Preserve architecture boundaries (UI -> coordinator -> services -> models)
- Do not introduce alternate command systems when
justrecipes already exist - Prefer extending existing services over adding parallel duplicate services
- Local release helpers:
just build-release,just export-app,just dmg,just dmg-self-signed(fallback only) - Manual release flow is
just release <X.Y.Z>(local execution, not CI-driven)- Create/edit contextual release notes (
release-notes/vX.Y.Z.md) - Run tests
- Build signed release DMG (
just dmgexports a Developer ID-signed app first) - Generate
appcast.xml - Create + push tag
- Create GitHub release via
ghwith notes + DMG +appcast.xml
- Create/edit contextual release notes (
- CI workflows under
.github/workflows/are for build/test validation; release publishing is manual - Sparkle appcast generation is scripted via
just appcast <dmg-path> - Keep
just build-self-signed/just dmg-self-signedonly as a fallback when Apple signing is unavailable
- Build passes:
just build - Relevant tests pass:
just test(and integration when touched) - No new warnings from your change scope
- Docs/comments updated only when behavior changes
- Keep diffs focused; avoid opportunistic formatting-only churn
- App lifecycle:
Pindrop/PindropApp.swift - Service composition:
Pindrop/AppCoordinator.swift - Settings and keychain:
Pindrop/Services/SettingsStore.swift - Audio capture core:
Pindrop/Services/AudioRecorder.swift - Transcription orchestration:
Pindrop/Services/TranscriptionService.swift - Logging facade:
Pindrop/Utils/Logger.swift - Localization:
Pindrop/AppLocalization.swift,Pindrop/Generated/LocalizationMetadata.swift,Pindrop/Generated/L10nKeys.swift,Pindrop/Localization/Localizable.xcstrings,Pindrop/Localization/InfoPlist.xcstrings,Localization/ - Localization tooling:
scripts/localization.py,justfile - Build recipes:
justfile - Contributor docs:
README.md,CONTRIBUTING.md,BUILD.md
- Use
justcommands in examples unless a directxcodebuildform is required - When adding tests, mirror structure from the nearest existing test file first
- Prefer Swift Testing assertions (
#expect,#require,Issue.record) for unit tests; keep XCTest only for UI automation - When touching settings, verify both app behavior and test-mode behavior
- When touching model or schema code, verify migration and read/write behavior
- When adding or changing user-visible strings, update both Swift
localized(...)keys andLocalizable.xcstrings(andInfoPlist.xcstringsfor permission / bundle strings) for all shipped locales
This project is indexed by GitNexus as pindrop (715 symbols, 699 relationships, 0 execution flows). Use the GitNexus MCP tools to understand code, assess impact, and navigate safely.
If any GitNexus tool warns the index is stale, run
npx gitnexus analyzein terminal first.
- MUST run impact analysis before editing any symbol. Before modifying a function, class, or method, run
gitnexus_impact({target: "symbolName", direction: "upstream"})and report the blast radius (direct callers, affected processes, risk level) to the user. - MUST run
gitnexus_detect_changes()before committing to verify your changes only affect expected symbols and execution flows. - MUST warn the user if impact analysis returns HIGH or CRITICAL risk before proceeding with edits.
- When exploring unfamiliar code, use
gitnexus_query({query: "concept"})to find execution flows instead of grepping. It returns process-grouped results ranked by relevance. - When you need full context on a specific symbol — callers, callees, which execution flows it participates in — use
gitnexus_context({name: "symbolName"}).
gitnexus_query({query: "<error or symptom>"})— find execution flows related to the issuegitnexus_context({name: "<suspect function>"})— see all callers, callees, and process participationREAD gitnexus://repo/pindrop/process/{processName}— trace the full execution flow step by step- For regressions:
gitnexus_detect_changes({scope: "compare", base_ref: "main"})— see what your branch changed
- Renaming: MUST use
gitnexus_rename({symbol_name: "old", new_name: "new", dry_run: true})first. Review the preview — graph edits are safe, text_search edits need manual review. Then run withdry_run: false. - Extracting/Splitting: MUST run
gitnexus_context({name: "target"})to see all incoming/outgoing refs, thengitnexus_impact({target: "target", direction: "upstream"})to find all external callers before moving code. - After any refactor: run
gitnexus_detect_changes({scope: "all"})to verify only expected files changed.
- NEVER edit a function, class, or method without first running
gitnexus_impacton it. - NEVER ignore HIGH or CRITICAL risk warnings from impact analysis.
- NEVER rename symbols with find-and-replace — use
gitnexus_renamewhich understands the call graph. - NEVER commit changes without running
gitnexus_detect_changes()to check affected scope.
| Tool | When to use | Command |
|---|---|---|
query |
Find code by concept | gitnexus_query({query: "auth validation"}) |
context |
360-degree view of one symbol | gitnexus_context({name: "validateUser"}) |
impact |
Blast radius before editing | gitnexus_impact({target: "X", direction: "upstream"}) |
detect_changes |
Pre-commit scope check | gitnexus_detect_changes({scope: "staged"}) |
rename |
Safe multi-file rename | gitnexus_rename({symbol_name: "old", new_name: "new", dry_run: true}) |
cypher |
Custom graph queries | gitnexus_cypher({query: "MATCH ..."}) |
| Depth | Meaning | Action |
|---|---|---|
| d=1 | WILL BREAK — direct callers/importers | MUST update these |
| d=2 | LIKELY AFFECTED — indirect deps | Should test |
| d=3 | MAY NEED TESTING — transitive | Test if critical path |
| Resource | Use for |
|---|---|
gitnexus://repo/pindrop/context |
Codebase overview, check index freshness |
gitnexus://repo/pindrop/clusters |
All functional areas |
gitnexus://repo/pindrop/processes |
All execution flows |
gitnexus://repo/pindrop/process/{name} |
Step-by-step execution trace |
Before completing any code modification task, verify:
gitnexus_impactwas run for all modified symbols- No HIGH/CRITICAL risk warnings were ignored
gitnexus_detect_changes()confirms changes match expected scope- All d=1 (WILL BREAK) dependents were updated
After committing code changes, the GitNexus index becomes stale. Re-run analyze to update it:
npx gitnexus analyzeIf the index previously included embeddings, preserve them by adding --embeddings:
npx gitnexus analyze --embeddingsTo check whether embeddings exist, inspect .gitnexus/meta.json — the stats.embeddings field shows the count (0 means no embeddings). Running analyze without --embeddings will delete any previously generated embeddings.
Claude Code users: A PostToolUse hook handles this automatically after
git commitandgit merge.
| Task | Read this skill file |
|---|---|
| Understand architecture / "How does X work?" | .claude/skills/gitnexus/gitnexus-exploring/SKILL.md |
| Blast radius / "What breaks if I change X?" | .claude/skills/gitnexus/gitnexus-impact-analysis/SKILL.md |
| Trace bugs / "Why is X failing?" | .claude/skills/gitnexus/gitnexus-debugging/SKILL.md |
| Rename / extract / split / refactor | .claude/skills/gitnexus/gitnexus-refactoring/SKILL.md |
| Tools, resources, schema reference | .claude/skills/gitnexus/gitnexus-guide/SKILL.md |
| Index, status, clean, wiki CLI commands | .claude/skills/gitnexus/gitnexus-cli/SKILL.md |