ultra-trace
ultra-trace does deterministic Swift static analysis with an assist/gate split.
The problem
Swift teams need static analysis that can gate a build offline and still help a human explore risky paths. Chat-only tools blur those jobs and send code to a model.
I needed those contracts kept separate (assist for exploration, gate for CI), and I did not want the model anywhere near the exit code.
Why I built it
Day to day, I use explore when I want to understand paths with optional LLM help, and I use analyze when I need to gate the build.
The two commands share one engine with different contracts, which gives me one analyzer I can trust in CI and can still lean on when I want another careful look at a risky path. I was counting on CI meaning an offline, deterministic exit that optional assist never owns.
What I built
ultra-trace is a deterministic static analysis CLI for Swift (Core), with optional Advanced SIL, bounded call summaries, and a Python frontend. Entry points are ultra-trace and python -m ultra_trace. Building the SwiftSyntax helper requires macOS and Xcode.
Dual mode covers two contracts. explore is day-to-day assist, where the LLM is optional and results stay advisory, and the process uses a soft exit unless you pass --fail-on-findings. analyze is the CI / nightly gate, and it runs offline with the LLM off on the same analysis engine.
Reports write markdown, JSON, and SARIF, and high findings can carry proof artifacts with tiers. Privacy modes are offline, redacted, and full-assist, and API keys live only in the appdir .env and never in YAML. Phases 1 through 6 are complete, and Phase 7 covers platform maturity, meaning Mac fleet bootstrap, perf budgets, SARIF export, and operator hygiene.
You launch it with ultra-trace or python -m ultra_trace.
Three decisions
These are the three choices I would defend in a design review.
1. Dual mode is the product.
Assist and gate share one engine and keep different contracts. explore is human-first and advisory, while analyze owns CI exit codes and severity thresholds. The split was part of the design from the start and was never a flag added later. I split the modes so optional assist never gets to own whether the build fails. The gate has to guarantee an offline exit code driven by severity thresholds the model cannot rewrite, and I would take one engine with two contracts over two products that grow apart. Keeping the contracts separate stops a chat tool from drifting into the gate, and it stops the gate from shipping half-baked LLM opinions into the exit code.
2. The analyzer owns findings, and the LLM stays advisory.
When assist is on, the model may propose an exploration plan, draft advisory wording, or emit optional llm.proposed_candidate items, but it never changes severity, confidence, or proof tier. Proposed candidates never fail Core severity gates on their own, and offline wins over --llm-enabled. Severity and proof have to come from the deterministic engine, or CI stops meaning anything. I made offline win over the flag so habit cannot turn the gate into an online call. The model can help me look, but it cannot rewrite the grade, and that comes from respect for what a gate has to mean more than from distrust of the model.
3. Proofs and privacy are engineered into the product.
High findings get a proof tier, which is one of runnable scaffold, repro steps, path witness, or unsupported. Keys stay in the appdir .env, offline blocks the network and forces the LLM off, and redacted and full-assist sanitize outbound text. Toolchain pins, drift checks, and SARIF support a Mac fleet and Code Scanning. If a finding can fail a build, I want a path I can inspect offline, and proof tiers let me answer why a build failed without relying on a hunch. The privacy modes exist so assist never sends more code than the chosen mode allows, and keeping keys in the appdir stops YAML from turning into a secret store. Pins and SARIF are there for operators who have to run this on a fleet rather than for a one-off demo, which is why I built them into the product and did not leave them as afterthoughts in the docs. The offline path matters most, because trust in a gate starts there.
Proof
You can follow the dual-mode recipes and walk the same engine path yourself.
ultra-trace / python -m ultra_trace) and goes through config and modes (explore, analyze, repl, serve) and then discovery and the Swift/Python frontends. From there it reaches the analyzer-owned engine and the rule packs and proofs, and it ends at md / json / SARIF. LLM assist stays advisory only and never owns severity, confidence, or proof tier, and optional Advanced SIL falls back to Core when it is unavailable.
The public repo covers the rest in writing.
- It documents dual-mode recipes and named profiles (
assist,ci,ci-full). - Fixtures ship alongside
ci_analyze_samples.sh. - SARIF export comes with a GitHub Code Scanning example.
- Toolchain pins, bootstrap scripts, and operator guides are included.
- The README documents exit codes and known limitations.
Why I built it this way
I hold the same boundaries in my iOS work. I want clear ownership and a contract that persists, and I want a design a technical peer can judge quickly.
Dual mode keeps assist from owning CI, and analyzer-owned findings keep the model advisory, so assist and the CI gate stay separate contracts on one deterministic core. The optional LLM can advise, but it cannot rewrite the gate. I engineered proofs and privacy in because a finding that can fail a build should come from a path you can trust offline.
The code lives at github.com/rdegraci/ultra-trace, and you can go back home from here.