English | Русский
Code intelligence for local Swift projects: a repository map, structural search (a grep replacement) and semantics — where a symbol is defined, who uses it, what a package exposes. The point is to give an LLM agent precise, on-demand access to code instead of grep: better answers, fewer tokens.
Reusable by design — it works against the root of any project rather than being wired into one.
One question, one answer — instead of a grep-and-read-five-files loop. Both runs below are sextant against its own repository:
$ sextant context ProjectConfig
[index: spm · 1 store(s) · fresh]
── ProjectConfig [struct]
def: Sources/SextantCore/ProjectConfig.swift:4 public struct ProjectConfig: Codable, Sendable {
usages: 12
• Sources/SextantCore/ProjectConfig.swift:31 return .loaded(try JSONDecoder().decode(ProjectConfig.self, from: data))
• Sources/sextant/IndexCommands.swift:47 switch ProjectConfig.read(projectRoot: root) {
• Sources/sextant/MCPServer.swift:63 switch ProjectConfig.read(projectRoot: project) {
• Sources/sextant/main.swift:41 func loadConfig(_ arguments: [String]) -> ProjectConfig? {
…
bases and protocols: Sendable
$ sextant blast SourceLocation
── blast radius: SourceLocation [struct]
a change would touch: 9 files · 37 usages · 0 calls
Sources/SextantCore/BlastRadius.swift
Sources/SextantCore/IndexStore.swift
Sources/SextantCore/SymbolContext.swift
…Add --json to any of it and an agent gets the same answer as structured data. The same
queries are exposed to Claude Code as MCP tools — see MCP.
Measured on five public Swift packages (Alamofire, swift-argument-parser, swift-numerics, swift-nio, swift-syntax) at pinned commits. Full numbers and the commands to re-run them yourself are in docs/benchmarks.md:
| Task | Saving vs reading the sources |
|---|---|
| Learn a package's public surface | 79–91% |
| Learn one type, including all its extensions | 83–95% |
| Repeat a query on an unchanged tree | up to 112× faster (content-hash cache) |
The unit is bytes of output, because bytes are exactly reproducible while a token count
depends on the tokenizer. The comparison holds for the task "understand the surface", not
"understand the implementation": api returns signatures and doc summaries, not bodies. For
bodies there is body.
Working CLI and MCP server, version 0.7.x. 21 commands:
| Command | What it does | Layer |
|---|---|---|
map |
repository map under a token budget; --semantic — types by usage; --pagerank — files by centrality |
syntax / semantics |
api |
public surface of a package (attributes, doc summaries) | syntax |
search <pattern> |
structural search over the AST ($X, variadic $$$, statement patterns) |
syntax |
lint |
structural hygiene rules (--rules <json>) |
syntax |
refs / defs / callers |
usages / definition / call sites (callers account for protocol dispatch) | semantics |
callees |
what a symbol calls (best effort: calls within the project) | semantics |
impls / supertypes |
implementations and subtypes, bases and protocols of a type | semantics |
hierarchy <symbol> |
transitive call graph (--callees / --callers, --depth N) |
semantics |
context <symbol> |
one-shot summary: definition, usages, callers, callees, hierarchy | semantics |
blast <symbol> |
impact analysis: what a change to this symbol touches | semantics |
body <symbol> |
full text of a declaration (signature and body) | semantics + syntax |
construct <type> |
construction and injection sites (heuristic: Type() |
heuristic |
changed |
symbol-level git diff: what was added, removed, or changed signature | syntax |
golden / bench |
semantic regressions against a spec / latency and output volume | measurability |
mcp |
MCP server (stdio) for Claude Code — the semantic layer as tools | integration |
init |
set up a project: .sextant.json, registration in .mcp.json, and a check |
integration |
serve |
daemon with a warm index: cold CLI start 2.6s → 0.27s (measured on sextant itself) | integration |
doctor |
self-check of the setup (sources, libIndexStore, index store, freshness) | diagnostics |
index |
build an index store: SPM (swift build) or an app target (--app, xcodebuild) |
build |
Common flags: --json (structured output), --scope <subdirectory>, --max-files <N>,
--no-build, --reindex (rebuild the index before the query). Defaults come from
.sextant.json. .gitignore is respected.
Three routes; only the third needs a Swift toolchain.
1. Homebrew (tap) — recommended:
brew tap RSafargalin/tap
brew trust RSafargalin/tap
brew install sextant
sextant --versionHomebrew refuses to load formulae from an untrusted third-party tap, so brew trust is
required — it is a one-time acknowledgement that you are installing from someone's personal
tap rather than homebrew-core.
2. Prebuilt binary from a release — macOS universal (arm64 + x86_64), no Homebrew:
V=0.7.0
curl -fsSL -O "https://github.com/RSafargalin/sextant/releases/download/v$V/sextant-$V-macos-universal.tar.gz"
shasum -a 256 "sextant-$V-macos-universal.tar.gz" # compare with the sha256 in the release notes
tar -xzf "sextant-$V-macos-universal.tar.gz"
xattr -d com.apple.quarantine sextant || true # clear quarantine (absent attribute is not an error)
mkdir -p ~/.local/bin && install -m 0755 sextant ~/.local/bin/sextantThe binary is neither signed nor notarised (there is no developer certificate). Downloaded
through a browser, Gatekeeper attaches a quarantine attribute and blocks the first run; clear
it with xattr -d com.apple.quarantine sextant. Downloads via curl and Homebrew are not
quarantined, but the command is harmless either way.
3. From source — needs Swift 6.2+:
swift build && swift test
make ci # build + test + self-lint
make install # release binary into ~/.local/bin (add it to PATH)
swift run sextant help
swift run sextant map --project <path>The syntactic commands (map, api, search, lint, changed) run on a bare system. The
semantic ones (refs, defs, callers, callees, impls, supertypes, hierarchy,
context, blast, body) need an Xcode toolchain: libIndexStore.dylib is located
through xcrun --find swiftc, and the index store is produced by swift build or
xcodebuild. To check a setup, run sextant doctor --project <path> — a checklist with
actionable hints about whatever is missing.
Every CLI invocation pays to open the index store — on a grown store that is around 2.6s, and every sub-agent pays it. The daemon keeps the index open:
sextant serve --project /path/to/project & # in the background, one per projectClients use it automatically; with no daemon running they fall back to the normal path, which
is not an error. SEXTANT_NO_DAEMON=1 disables the daemon entirely. Builds and setup
(index, init, doctor) are never executed by the daemon — they run foreign code and write
into the project.
Measured on sextant itself (53 MB store, identical state): refs 2.6s → 0.27s, roughly
10×. On a small freshly built store the difference is smaller, because reusing the database
between runs already gets you to ~0.3s.
sextant mcp is a stdio MCP server (JSON-RPC 2.0). The index is opened once at start-up and
reused, so a warm request costs ~0.2s against a cold shell-out. Freshness is handled by
listenToUnitEvents plus a poll before each request: an index built during the session is
picked up without restarting the server. 13 tools:
context, blast_radius, body, who_defines, find_references, find_callers,
list_implementations, call_hierarchy, repo_map, structural_search, lint,
api (the public surface of a package or type — an order of magnitude cheaper than reading
files) and changed (symbol-level git diff).
Tools honour .sextant.json (budget, scope, rules) exactly as the CLI does. The tool list in
initialize is generated from the contract, so it cannot drift from the actual set.
The easiest way in is one command at the project root. It creates .sextant.json, registers
the server in .mcp.json (any servers already there are preserved) and tells you what to do
next:
sextant initManually, through the client (after make install the binary is at ~/.local/bin/sextant):
claude mcp add sextant -- ~/.local/bin/sextant mcp --project /path/to/projectOr with a .mcp.json at the project root:
{
"mcpServers": {
"sextant": {
"command": "/absolute/path/to/sextant",
"args": ["mcp", "--project", "/path/to/project"]
}
}
}The semantic tools need an index store, so build one first (sextant index, or
index --app). Without an index the server still starts, repo_map works, and the semantic
tools return a hint rather than a wrong answer.
Before registering, check the setup with sextant doctor --project <path> — a checklist
(sources, libIndexStore, index store and its freshness) with hints about what to build.
| Layer | Purpose | Technology | Status |
|---|---|---|---|
| L1 repo map | symbol map under a token budget | SwiftSyntax | ✅ |
| L2 structural | structural search and rules, a grep replacement | own engine (SwiftSyntax) | ✅ |
| L3 semantic | defs / refs / callers / public API | IndexStoreDB | ✅ |
| L3+ semantic | callees / type hierarchy / transitive call hierarchy / PageRank map | IndexStoreDB relations | ✅ |
| L4 MCP | commands as tools for an agent (stdio JSON-RPC, warm index) | MCP (stdio) | ✅ |
| L5 | other languages, dead code, real time | — | ⬜ non-goal for v1 |
Non-goals for v1: real-time file watching, vector search, non-Swift semantics. These are
deliberate cuts rather than oversights; several are scheduled for later iterations in
docs/roadmap.md (which, along with the architecture decision records, is in Russian — it is
a historical record of how the tool got here).
See CONTRIBUTING.md for setup and expectations, and AGENTS.md for the invariants that are not visible from the source.
Apache License 2.0 — see LICENSE. The same licence as Swift and swift-syntax, which this tool is built on.