Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
a49cae6
docs(adr-0003): оба вопроса про демон закрыты замером — не строим
RSafargalin Aug 7, 2026
5f78ebb
docs(roadmap): бэклог того, что ждёт обстоятельств, а не работы
RSafargalin Aug 8, 2026
1e0eef0
docs(roadmap): свежесть в Iter 9 приведена в соответствие с ADR-0003
RSafargalin Aug 8, 2026
ed4a772
docs: фаза 1 зоны памяти закрыта замером — отыгрывать нечего
RSafargalin Aug 8, 2026
0efee1d
feat(index): назвать, что выполняет сборка, и чем это сдержано
RSafargalin Aug 8, 2026
dee642f
docs(roadmap): сверка документов с кодом — дефекта нет, гейт назначит…
RSafargalin Aug 8, 2026
a139151
docs(adr-0005): специализация на Apple-среде — ответ всегда про конкр…
RSafargalin Aug 8, 2026
23eaa8d
test: реестр дефектов CLI-поверхности — шестнадцать воспроизведений
RSafargalin Aug 13, 2026
e84fe92
test: реестр дефектов, зависящих от индекса — восемь воспроизведений
RSafargalin Aug 13, 2026
03655df
test: реестр дефектов слоя clang и поверхности MCP — пять воспроизвед…
RSafargalin Aug 13, 2026
9abfc5d
test: потолок ссылок и заморозка файлового списка демоном
RSafargalin Aug 13, 2026
3c190bc
fix(freshness): свежесть видит все языки, которые видит индекс
RSafargalin Aug 13, 2026
a03c0ca
fix(honesty): семь дефектов из реестра — ответ называет то, чего рань…
RSafargalin Aug 13, 2026
eaecac5
fix(honesty): числа, пределы и адресация — двенадцать дефектов реестра
RSafargalin Aug 13, 2026
c61f92a
fix(honesty): слой clang и поверхность MCP — шесть дефектов реестра
RSafargalin Aug 13, 2026
f367c3b
fix(honesty): реестр закрыт — машинный контракт, фрагменты и демон
RSafargalin Aug 13, 2026
f10b3ea
test(store): выбор стора всё-таки воспроизводится — тест был сломан, …
RSafargalin Aug 14, 2026
9679207
fix(store): выбор стора применяет тот же предикат, что фильтр записей
RSafargalin Aug 14, 2026
0ed727c
feat(store): политику выбора стора выбирает человек, и каждый ответ е…
RSafargalin Aug 16, 2026
0882e5f
feat(store): читатель юнитов libIndexStore и политика выбора по покрытию
RSafargalin Aug 16, 2026
6977148
fix(honesty): два дефекта, найденных проверкой на публичных репозиториях
RSafargalin Aug 16, 2026
5b71d69
feat(scale): предел файлов деградирует ответ вместо отказа, у api поя…
RSafargalin Aug 16, 2026
18375ef
fix(hierarchy): позиция в иерархии — это место определения, а место в…
RSafargalin Aug 16, 2026
74b6a8b
fix(counts): ссылка, записанная раскрытием макроса, больше не считает…
RSafargalin Aug 16, 2026
af1b943
fix(index): граница проекта — репозиторий, путь DerivedData настраива…
RSafargalin Aug 16, 2026
ae70b3e
fix(flags): у базы компиляции появилось время, и мёртвые записи не пе…
RSafargalin Aug 16, 2026
fe340df
fix(verify,mcp): недосчёт под #if назван; сервер перечитывает выбор с…
RSafargalin Aug 16, 2026
032c376
docs(adr-0007): Interface Builder не читаем — сказано прямо, а не ост…
RSafargalin Aug 17, 2026
2ad90ed
docs(readme.ru): русская версия догнала английскую
RSafargalin Aug 17, 2026
19c473b
docs(measurements): общая база IndexStoreDB — вред не подтверждён, ст…
RSafargalin Aug 17, 2026
4d1f99f
chore(release): 0.9.0
RSafargalin Aug 17, 2026
7f13365
fix(daemon): захват вывода не зависит от свободных потоков в пуле
RSafargalin Aug 17, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .sextant.json
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
{
"budget" : 6000,
"maxFiles" : 4000
"maxFiles" : 4000,
"storePolicy" : "recency"
}
177 changes: 177 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,61 @@ Human-readable text output is not covered — parse `--json`, not prose.

## [Unreleased]

## [0.9.0] — 2026-08-17

An answer now says what it stands on. Which index store it came from and how much of the project
that store covers; whether a number is the whole count or the part that fit; where a symbol is
defined as opposed to where it is called. The work started as one bug — the wrong store was chosen
on a project with agent worktrees, so every semantic answer was empty under a `fresh` label — and
turned into a pass over every place the tool answered confidently without being able to. Forty
defects were reproduced by running the tool, written down as tests that fail when they are fixed,
and closed. Six public repositories and the reference project were used to check that they stay
closed.

### Added

- **`sextant store` and an explicit store policy.** A project routinely has more than one index
store — `swift build` writes one, an editor's indexer another, a second checkout its own — and
they answer the same question differently: measured on this repository, 83 references from one
and 34 from another, 91 from both merged. So the tool no longer chooses. With one usable store
nothing is asked; with several and no policy, semantic commands refuse and print what each policy
gives, what it does not and what it costs. `store use recency|union|coverage` records the choice
in `.sextant.json`; `--store-policy` overrides it for one command, `SEXTANT_STORE_POLICY` for one
machine. Whenever there was a choice, every answer carries the line that says how many candidates
there were, which are being read and why the others were not
([ADR-0006](docs/adr/0006-store-policy-is-a-persons-decision.md)).

- **Coverage of the opened store, in the trust label.** `[index: derivedData · 1 store(s) · fresh ·
covers 10337/12351 files (84%)]`. Freshness is about time, coverage is about extent, and a fresh
store can hold half a project: a release build compiles no test target, so every reference from a
test is missing from a store nothing else marked as partial. Measured on this repository, the same
query against two stores of one project: `covers 92%` → 74 usages in 23 files, `covers 55%` → 46
in 17. It is read from the units, cached against the store's own state, and costs nothing on every
run but the first after a build.

- **A units reader for libIndexStore** (`CIndexStoreShim`, `dlopen` over a declared subset of the
ABI, as the libclang layer already does). IndexStoreDB answers about symbols and cannot tell "this
store does not cover the file" from "the file has no such symbol"; a unit answers directly — this
is the file it was built from. It is what makes the `coverage` policy and the label above possible.

- **`api --budget <tokens>`** bounds the printed surface and the header then states what was left
out. There is no default: a public surface is a contract, and cutting one silently changes what
every existing answer says. Over MCP the budget does have a default — an agent reads the answer
into a context window.

- **`--derived-data` and `SEXTANT_DERIVED_DATA`.** The DerivedData location was hard-coded, so a
project built with `xcodebuild -derivedDataPath ./build` — which every CI does — looked like a
project with no index at all.

- `index` now states what it runs and what holds it back, and it says something different for each
path, because the two are not equally protected. Measured with a hostile manifest and a hostile
build plugin: SwiftPM sandboxes both — no network, no writes into your home directory — but they
can read it; `--app` runs `Run Script` phases with your full access and no sandbox at all. Both
notices name `--no-build`, which indexes an already-built project and runs nothing. Three MCP
tool descriptions sent an agent to run `sextant index` without ever saying it builds the project;
they say it now. Wrapping the build in `sandbox-exec` was measured and is impossible — SwiftPM
applies its own sandbox and a nested one is denied by the kernel.

- `init --client <name>` registers the MCP server for a client other than Claude Code. The list is
`claude-code` (default, a file in the project) and `claude-desktop` (one list per machine, so the
project is named in the arguments). Everything the `.mcp.json` path was already careful about now
Expand Down Expand Up @@ -46,6 +99,130 @@ Human-readable text output is not covered — parse `--json`, not prose.
`--scope` and `--index-store` complete directories while `--rules` and `--spec` complete files.
Closes [#2](https://github.com/RSafargalin/sextant/issues/2).

### Fixed

- **The wrong index store was chosen on a project with nested worktrees, and every semantic answer
was empty.** An agent worktree lives inside the checkout, so its DerivedData passed the "inside
the project" test, and being rebuilt more recently it also won on freshness — after which the
record filter rejected every path in it as foreign. On the reference project `refs
AbsAccountsViewModel` returned nothing and explained it as "a closure, a local, or another kind of
symbol" about a class; it now returns the definition and 7 usages in 3 files. Selection applies the
same scope predicate the filter applies; where a foreign store is used anyway, the empty answer
names it instead of inventing a reason.

- **A call hierarchy pointed at call sites and called them definitions.** `timestamp(ofStore:)` was
shown at line 48 — where it is called — while it is defined at line 25, and nothing said which of
the two it meant. The definition is now resolved by USR, the call site is a separate field, and a
symbol the index has no definition for is marked rather than given a made-up position.

- **A reference written inside a macro was counted twice.** A macro records the reference at its own
position as well as at the real column, so every count over code wrapped in `#expect`/`#require`
came out high: 81 against 74 occurrences of the name on this repository. A position whose column
does not carry the symbol's name is dropped when another position on the same line does; a lone
unverified position is kept, because the name may be written in a form the check cannot see.

- **`api --json` served internal members of public types.** The visibility filter was applied to the
top level only, so the text answer dropped them and the machine contract carried them — one
question, two answers. Found on Alamofire.

- **Counts stopped presenting a part as the whole.** `construct` counted one position twice when a
line held two constructions; `--limit` rewrote the header count instead of shortening the list;
the internal cap of 1000 references was printed as the total and is now named (`1000 of 1265`).

- **Structural search and lint.** A pattern the parser only recovered from (an unbalanced bracket)
answered `No matches.` with exit 0 and now fails with the reason — while a pattern that is not
Swift but is valid Objective-C still goes to clang. `search --limit` was accepted and ignored.
An exclusion that removed every result is now named. A `lint` rule whose pattern never compiled
was counted in the header as if it had run.

- **`changed`** now sees a symbol leaving the public surface (the access level is part of the
compared signature), follows a renamed file instead of reporting a rewrite, and refuses to diff a
file that does not parse — a recovery tree reported the half it dropped as deleted symbols, which
is exactly the state an agent's half-finished edit leaves behind.

- **Freshness and staleness.** The freshness signal walked `*.swift` only, so an edit to a `.m` or a
header left the label saying `fresh`. An answer that points at a deleted file now says so — a
deletion moves no timestamp, so the marker cannot see it. A snippet is withheld when the recorded
line no longer carries the symbol, instead of printing whatever text now sits there.

- **Compile flags had no notion of time.** They are captured by `sextant index`; every other build
moves the index forward and leaves them behind, and a `-DFEATURE=1` that has since become `0`
yields a structural match inside code the current build does not contain. A build newer than the
capture is now named, with the gap. Entries for files that no longer exist are dropped when the
database is read — measured on this machine, two databases held 75 of 75 and 72 of 73 such entries.

- **`--verify` was silent in the two directions that matter.** More semantic hits than textual ones
cannot be true of a name written where it is used, and it printed the pair without comment. Fewer
semantic than textual, with the difference sitting inside `#if` branches this build does not
contain, fell exactly on the wrong side of a ×3 threshold (the measured case was 1 against 3);
those lines are now counted and named.

- **`--project` pointed at a directory containing several projects borrowed a foreign index.** A path
prefix is not a project boundary: `--project ~` offered the store of an unrelated app from
`Downloads`, covering 6% of what it called "this project". The boundary is the checkout.

- **The MCP surface.** Answers carry the provenance line the CLI prints — a client logs stderr at
best, so the agent, the main consumer, never saw where an answer came from. `repo_map` gets the
index the CLI gets and names the non-Swift files missing without it. `list_implementations`
distinguishes an unknown symbol from one without implementations. Area tools are bounded by
`maxFiles` and say what they did not read. The store policy refusal reaches the answer instead of
the log. A `.sextant.json` edited while the server runs takes effect on the next call.

- **Smaller refusals that used to pass silently:** an empty symbol, a single-dash flag spelling
(`-project`), a `--project` that does not exist, an unmatched `api --package`, a misspelled key or
policy value in `.sextant.json`. The daemon re-reads the file list per request instead of freezing
it for its lifetime. A package is addressed by the name in its manifest rather than by the first
path component, so a project that keeps packages outside `Packages/` can address them at all.

- **The daemon's output capture could hang, and did hang CI.** Draining ran as a block on the
global queue while the caller was already blocked on the write, so anything holding the pool's
threads — several `Process.waitUntilExit` calls, say — starved the reader that would have
unblocked it. Reproduced with 80 blocked tasks: the write never returned, and a watchdog on the
same queue never fired either. Each reader now gets a thread of its own. The test written for the
original defect passed locally in 0.002s and timed out at 60 seconds on the runner, twice; the
starvation itself is now a test.

- **The clang layer counted a file nobody could open as scanned**, and a byte that is not valid
UTF-8 shifted every offset after it, so a column and a snippet were both wrong while presented as
structural.

### Changed

These are the stability contract — CLI flags, JSON schemas, MCP tool names, exit codes — and this
release breaks some of it, which is why the minor version moves.

- **Exit codes.** Cases that used to answer 0 now fail: an unparsable structural pattern, an empty
symbol, a single-dash flag, a `--project` that does not exist, an `api --package` that matches
nothing. Semantic commands exit non-zero when several index stores are usable and no policy is
set.

- **`--max-files` degrades instead of refusing.** `map`, `api`, `search` and `lint` used to answer
"more files than the limit" and nothing else; they now read the limit and name what they did not
(`⚠ covered 4000 of 12351 file(s)`), which changes their exit code on a large project from 1 to 0.
The bound is a prefix of the file list in a fixed order, so two runs cover the same files.

- **`--json` shapes.** `refs`/`defs`/`callers` return an object rather than an empty array when the
semantic answer is empty and the textual degradation applies, so a consumer cannot read textual
matches as resolved ones. `blast`, `hierarchy` and `context` return `{"symbol": …, "found": false}`
for an unknown symbol where they used to print prose on stdout. `lint --json` carries
`brokenRules`; `map`/`api` summaries carry the package name from the manifest.

- **Interface Builder is documented as out of scope.** `.storyboard` and `.xib` are not read and not
planned; a class named only from a nib is invisible. Measured on the reference project: 376 classes
are bound from nibs and none of them lives only there, so the cost is an undercount in impact
analysis rather than a false "nobody uses this"
([ADR-0007](docs/adr/0007-no-interface-builder.md)).

- **Benchmarks re-measured** for scenarios A and B at the same pinned commits. Every surface is
larger than at 0.7.0 (Alamofire 973 → 1 226 declarations) because declarations behind `#if` and
internal extensions carrying public members are now part of the surface; the saving band moves
from 79–91% to 79–90%.

- A shared IndexStoreDB directory between processes was recorded as a defect and is not one:
measured, per-process databases are worse at every level of concurrency (4 processes: 34.4s shared
against 73.8s separate), answers never diverged, and nothing indicated a re-import or corruption.
The stand is in [docs/measurements](docs/measurements).

## [0.8.1] — 2026-08-07

### Added
Expand Down
Loading
Loading