I've spent 18 years shipping iOS apps — enterprise VoIP, automotive, retail — and one problem kept resurfacing regardless of the domain: codebases quietly rot, and Swift never had great tooling to catch it early. Most static analysis tools are style linters, not architecture watchdogs. Nothing was really looking for God Objects, tangled responsibilities, or classes that had silently outgrown their purpose.
So I built AIAnalyzer — a Swift CLI, distributed via Homebrew, that does two things: detects real architectural smells using SwiftSyntax, and offers AI-generated fix suggestions through a system that decides, in real time, whether to trust a fast local model or escalate to a stronger cloud one.
The problem with "just use AI for everything"
Bolting an LLM onto a dev tool is easy. Bolting one on responsibly is a different problem. Two extremes are both bad:
Cloud-only: great quality, but every request leaves the machine. For teams under compliance constraints — banks, healthcare, defense — this is a non-starter.
Local-only: private and fast, but smaller on-device models sometimes give shallow or wrong suggestions, and you have no way of knowing when that's happening.
I wanted a third option: a system that tries local first, evaluates its own output quality, and only spends the cloud API call when it actually needs to.
The architecture
AIAnalyzer's AI layer has four provider types behind a shared AIProvider protocol:
swift
public protocol AIProvider {
/// Requests a refactoring suggestion based on the provided context.
/// - Parameter context: The issue and source code details.
/// - Returns: An `AISuggestion` containing the AI's response.
/// - Throws: An `AIProviderError` if the request fails.
func suggest(for context: AIRequestContext) async throws -> AISuggestion
}
A factory dynamically builds the right provider based on configuration:
swift
let provider: AIProvider
switch configuration.serviceConfig.providerType {
case .gemini:
// Cloud provider
provider = GeminiProvider(apiKey: apiKey, model: configuration.serviceConfig.model)
case .ollama:
// Local provider backed by Ollama, running a full LLM on-device
provider = OllamaProvider(endpoint: configuration.serviceConfig.ollamaEndpoint, modelName: configuration.serviceConfig.ollamaModel)
case .local:
// Local Core ML provider — a lighter-weight on-device option with heuristic fallback
provider = LocalLLMProvider(modelPath: configuration.localModelConfig.localModelPath, modelName: configuration.localModelConfig.localModelName)
case .hybrid:
// Orchestrator combining a local preference with an optional cloud fallback
provider = HybridAIProvider(
localPreferred: localPreferred,
localFallback: localFallback,
cloud: cloud,
preferLocal: true
)
}
Gemini — cloud API, highest quality, requires network + key
Ollama — full LLM running fully on-device via Ollama, zero network calls
Local (Core ML) — a lighter-weight on-device option with its own heuristic fallback, useful when you want on-device inference without running a full Ollama-hosted model
Hybrid — attempts a local-preferred provider first, scores confidence based on output characteristics, escalates to Gemini only when confidence is low, and falls back to a local-fallback or static message if all else fails
The interesting engineering problem isn't "call an LLM" — it's the confidence heuristic that decides whether to trust the local pass, and having two different flavors of local (a full LLM via Ollama, and a lighter Core ML path) gives the Hybrid mode real flexibility depending on what's available on a given machine. That layered fallback is the actual differentiator, and it's the part I'd want to keep iterating on if this becomes a bigger project.
Turning the tool on itself
Once detection worked, I ran AIAnalyzer against its own source — and found the exact smell it was built to catch. AnalyzerApp.swift had grown to 695 lines, quietly doing argument parsing, config loading, AI provider construction, and orchestration all at once. A God Object, in the God Object detector's own codebase.
Fixing it meant:
Migrating hand-rolled CLI parsing to Apple's swift-argument-parser
Splitting responsibilities into AnalyzerCommand, EnvironmentResolver, AISuggesterFactory, and AnalysisOrchestrator
Along the way, I also found and fixed a real false-positive bug: type classification was using substring matching on class names, so DetailViewControllerDelegate was getting misclassified as an oversized ViewController. Fixed by combining syntactic inheritance checks with strict suffix matching instead of loose contains() checks.
What shipped
59 passing tests, including boundary and integration cases
Fully automated release pipeline — tagging a version builds the binary, computes the checksum, and pushes the Homebrew formula update via GitHub Actions with zero manual steps
SARIF output, so findings integrate directly into GitHub code scanning
Publicly installable: brew install elangbamjohnson/tap/aianalyzer
What I'd tell someone building something similar
If you're integrating AI into a dev tool, the confidence-based fallback pattern is worth the extra complexity. It's tempting to just pick cloud or local and move on, but the real value — for privacy-conscious teams especially — is in a system that's honest about when it doesn't know enough, rather than confidently returning a weak local answer or defaulting to the network every single time.
If you want to see more of what I've been building — including a
deeper case study on this project —
my portfolio's at (https://elangbamjohnson.github.io/)
Repo's here (https://github.com/elangbamjohnson/AIAnalyzer)
Top comments (0)