Skip to content

0.9.0 — an answer that says what it stands on: store choice, honest counts, bounded answers - #23

Merged
RSafargalin merged 32 commits into
mainfrom
claude/daemon-measured
Aug 17, 2026
Merged

RSafargalin merged 32 commits into
mainfrom
claude/daemon-measured

Conversation

@RSafargalin

@RSafargalin RSafargalin commented Aug 7, 2026

Copy link
Copy Markdown
Owner

Started as one bug and became a pass over every place the tool answered confidently without being
able to. It now carries release 0.9.0.

The bug it started with

On a project with agent worktrees the wrong index store was chosen: a 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.

refs AbsAccountsViewModel        before: ⚠ 0 semantic hits → "a closure, a local, or another kind of symbol"
                                 after:  def: …/AbsAccountsViewModel.swift:16 · usages: 7 in 3 file(s)

Selection now applies the same scope predicate the record filter applies. The earlier note that this
"could not be reproduced by a test" was wrong: the test was broken, not the tool —
createSymbolicLink(at:withDestinationURL:) resolves a relative destination against the current
directory, so the rival store never existed on disk.

What grew out of it

Forty defects reproduced by running the tool, written down as tests that fail when the defect is
fixed
, and closed. Highlights:

  • Store choice is a person's decision. Several usable stores answer the same question
    differently (measured here: 83 references from one, 34 from another, 91 from both), so the tool
    refuses to guess and sextant store use recency|union|coverage records the choice. Every answer
    carries which store was read and why the others were not (ADR-0006).
  • A units reader for libIndexStore (dlopen over a declared ABI subset) makes coverage
    measurable, so the trust label now says extent as well as freshness:
    [index: derivedData · 1 store(s) · fresh · covers 10337/12351 files (84%)].
  • Counts stopped overstating. A reference written inside a macro was counted twice (81 against
    74 real occurrences); --limit rewrote the total instead of shortening the list; the internal cap
    of 1000 was printed as the whole count.
  • A hierarchy pointed at call sites and called them definitionstimestamp(ofStore:) at line
    48, where it is called, while it is defined at 25.
  • Limits degrade instead of refusing. map, api, search, lint on a large project 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)).
  • Compile flags gained a notion of time, and dead entries no longer survive a read (two
    databases on this machine held 75 of 75 and 72 of 73 entries for files deleted weeks earlier).
  • The daemon's output capture could hang — draining ran on the global queue while the caller was
    blocked on the write, so a starved pool starved the reader. It hung this very PR's CI twice.

Why the minor version moves

The stability contract breaks in three places, all listed in the changelog: exit codes (an
unparsable pattern, an empty symbol, a single-dash flag, a missing --project, an unmatched
api --package now fail), --max-files (refusal → bounded answer, so code 1 → 0 on a large
project), and --json shapes (the degraded refs answer is an object; blast/hierarchy/context
return {"found": false} where they printed prose).

Verification

  • 318 tests, make ci green, CI green on the runner.
  • The battery of fixed defects re-run on five public repositories (Alamofire, swift-argument-parser,
    swift-numerics, swift-nio, swift-syntax) and on the reference project: 172 checks, zero failures.
    Two real defects were found that way and fixed here.
  • Benchmarks re-measured for scenarios A and B at the same pinned commits; recipes re-run against
    Alamofire 0455bfb — three of twelve blocks had moved and were updated.
  • Interface Builder is documented as out of scope (ADR-0007);
    the shared IndexStoreDB directory was measured and is not a defect — per-process databases are
    worse at every level of concurrency, and the stand is kept in docs/measurements.

After merge

Tag v0.9.0 on main (the release workflow checks it against Sextant.version), then update
Formula/sextant.rb from the block the workflow prints — the sha256 cannot exist before the
archive does.

RSafargalin and others added 5 commits August 8, 2026 04:38
Замер на самом sextant вместо предположений:

  MCP initialize (открытие индекса)  4.52с — один раз за сессию
  MCP вызов инструмента              0.08–0.23с
  CLI через демон, повтор            0.10–0.33с (0.05с — старт клиента)
  CLI без демона                     3.2с каждый раз

Объединение serve и MCP сэкономило бы 4.5с однократно и один экземпляр
IndexStoreDB — ценой моста между протоколами, управления жизненным циклом
и зависимости MCP от поднятого демона. Не окупается.

Разделяемый кэш AST в демоне: дисковые кэши по content-hash уже делают
повтор дешёвым, и на общий кэш в памяти остаётся доля от 0.05–0.25с —
против разделяемого изменяемого состояния между запросами, то есть класса
ошибок, который демон уже однажды принёс.

Выигрыш даёт сам демон (3.2с → 0.1–0.3с), а не то, что внутри него
разделено, — и он уже есть. Условия для возврата записаны.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Вторая половина гейта П3 (машина без Xcode-тулчейна), накопление данных
adoption и незаписанный вопрос про Linux — ни один из них не блокирован
решением или кодом, каждому нужно, чтобы сначала что-то случилось в мире.

Записаны в бэклог, чтобы не всплывать на каждом планировании.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Роадмап продолжал обещать build-hook (#17) и частичную свежесть по
timestamps юнитов (#9) как приоритет Iter 9 — при том что рамка Н5
опровергнута замером ещё в июле и в ADR записано «не делаем»: обычная
сборка уже обновляет index store, чинить нужно было сигнал свежести.

Раздел не-целей утверждал то же самое («автосвежесть → build-hook»).
Документ обещал работу, от которой инструмент уже отказался.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
ADR-0003 требовал двух чисел до любого кода зоны памяти: повтор-рейт и
потолок пользы от дедупа. Числа получены по 26 реальным агентным сессиям
(≈6.1 МБ навигационного контента, транскрипты локально, только размеры).

Три независимых определения повтора сходятся:

  тот же вызов с теми же аргументами        0.9 % навигационных байт
  побайтово идентичный результат в сессии   0.5 %
  то же, но в более поздней сессии          0.5 %
  перечитан неизменившийся файл             5.5 % байт чтений

Хэндлы, дельты и регидратация после компакции отыграют единицы процентов —
до вычета их собственной цены (~15–20 ток. на ответ). Гейт Iter 8 звучит
как «измеренное падение токенов» и недостижим.

Причина в тех же числах: агент редко просит дважды, а перечитывает обычно
то, что сам только что изменил, — чему хэндл служить не может.

Трата не в повторе, а в первой выдаче: 6.1 МБ отдано, на повторы 0.03 МБ.
Значит работа не «не отдавать второй раз», а «отдать меньше в первый» —
context-compiler (#16, Iter 10), этим замером не задетый.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Все команды, кроме index, читают. index запускает код проекта: манифест,
плагины сборки и макросы — код, а с --app фаза Run Script это shell-скрипт.
Сказано об этом было только в details справки.

Замер (macOS 26.5.2 / Swift 6.2.3, враждебный манифест и враждебный плагин):

                       манифест   плагин
  запись в $HOME       запрещена  запрещена
  сеть                 запрещена  запрещена
  чтение ~/.ssh        РАЗРЕШЕНО  РАЗРЕШЕНО
  чтение ~/.gitconfig  —          РАЗРЕШЕНО
  запись в /tmp        —          РАЗРЕШЕНА

То есть SwiftPM уже держит манифест и плагины в песочнице: прочитать дом
можно, записать в него и отправить прочитанное — нет. Путь --app не защищён
ничем: Run Script выполняется с полными правами.

BuildTrust в Core — единственный источник этих фактов. Два разных
уведомления вместо одного: уравнивать пути значило бы врать про более
опасный. Оба называют --no-build, который индексирует уже собранный проект
и не выполняет ничего, — он существовал и нигде не предлагался.

Три описания MCP-инструментов посылали агента выполнить sextant index, ни
разу не сказав, что это сборка чужого кода. Это самая узкая часть
поверхности: решение принимает агент по описанию.

Обернуть сборку в sandbox-exec, как предполагал ADR-0003, невозможно:
SwiftPM применяет свою песочницу, а вложенный sandbox_apply запрещён ядром —
swift build падает даже под профилем (allow default).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@RSafargalin RSafargalin changed the title docs(adr-0003): close both daemon questions by measurement — not worth building Measure before building: close three plan items, and say what index executes Aug 8, 2026
RSafargalin and others added 24 commits August 8, 2026 23:16
…ь не на что

Замер показал, что агенты читают Markdown постоянно: 18.5 % чтений и 27 %
прочитанных байт по 30 сессиям, во всех 14 проектах, и это README,
роадмапы, ADR и планы. Отсюда «сверять обещания документа с кодом»
выглядело очевидным следующим шагом.

Механические классы дрейфа посчитаны по трём независимым корпусам:

  sextant, все 57 коммитов                    0 битых ссылок, 0 file:line
  221 документ пяти публичных репозиториев    1 (похожа на ассет DocC)
  флаги в документации против каталога        0 расхождений
  двуязычные двойники после нормализации      0 расхождений

Детектору нельзя назначить гейт на дефект, которого не бывает.

Настоящий дрейф здесь семантический: роадмап неделями обещал build-hook
как приоритет Iter 9 после того, как ADR записал «не делаем». Чтобы
поймать такое, нужно прочитать два документа на противоречие — модель,
а это отдельное решение, не парсер Markdown.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…етную сборку

Широту по языкам держит Serena (LSP, 40+ языков, MIT, живая). В нише Swift
конкуренты мертвы: SwiftLens архивирован в марте, Xcode Index MCP заброшен,
Apple в Xcode 26.3 занял соседнюю поверхность — сборку и диагностику, не
навигацию по символам. Идти в широту значит идти со вторым фундаментом
против того, у кого он уже есть.

Взамен — стандарт качества внутри Apple-среды. Основание не в позиционировании,
а в трёх измеренных дефектах, найденных в тот же день:

  выбор стора    чужой стор с покрытием 57% выигрывает у своего с 98%
                 по mtime; refs 8 -> 0, context 11 строк -> 2, метка fresh
  конфигурация   release-стор: 80/121 исходников, тестовых 0/39;
                 refs 8 -> 6 и 3 -> 1 без единого маркера
  платформа      index --app по умолчанию собирает под macOS, SPM — под хост;
                 для iOS-проекта индекс описывает чужую платформу

Все три — один дефект с трёх сторон: index store всегда описывает одну
платформу, одну конфигурацию и один набор целей, то есть любой семантический
ответ — это ответ про конкретную сборку. Инструмент не говорит, про какую,
и не замечает, когда вопрос выходит за её пределы.

Отсюда назначение продукта: не поиск по коду для Swift, а ответы про
Apple-кодовую базу, честные относительно описываемой сборки. Ни у Serena,
ни у Apple этого нет.

Не-цели: языки вне Apple-среды, Linux, кросс-проект, graph-RAG, рефакторинг.
Гейт — три теста, а не заявление.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Каждый тест утверждает ПРАВИЛЬНОЕ поведение и обёрнут в withKnownIssue:
набор остаётся зелёным, пока дефект есть, и падает в тот момент, когда его
починят. Список дефектов, закрытие которых никто не замечает, — документ,
а не реестр.

Зафиксировано (всё воспроизведено запуском):

  --project на несуществующем каталоге    lint → "✅ No violations", код 0
  неразбираемый паттерн                   search 'print(' → "No matches"
  --limit у search                        объявлен, игнорируется
  исключения                              вырезают результат без упоминания
  --exclude Sources                       не исключает каталог вопреки doc
  правила, которые не скомпилировались    "rules: 1", отчёт чистый
  флаг с одним дефисом                    съедает паттерн, "No matches"
  пустой символ                           отвечает вместо ошибки аргумента
  опечатки в ключах .sextant.json         приняты молча
  api                                     включает internal и private типы
  api --json                              то же в машинном контракте
  api --package на несуществующем         "declarations: 0", код 0
  имя пакета                              берётся из пути, не из манифеста
  changed и уровень доступа               public -> private не замечен
  changed на неразбираемом файле          объявляет удалённой живую функцию
  changed и переименование                чистый rename читается как добавление

Тесты не требуют индекса: бинарь, временный пакет и git. Дефекты,
зависящие от индекса, пойдут отдельным набором — им нужна сборка.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Каждый тест строит одноразовый пакет с index store, утверждает правильное
поведение и обёрнут в withKnownIssue. Отсутствие тулчейна или неудачная
сборка считаются «неприменимо», а не провалом, — как в существующем
интеграционном наборе.

Зафиксировано:

  удаление исходника          индекс остаётся fresh, определение выдаётся
                              из несуществующего файла
  правка .c                   свежесть слепа: hasSourceNewer обходит *.swift
  body после сдвига строк     печатает тело ДРУГОГО символа
  снимки строк                текст текущего файла по устаревшим координатам
  --json                      пустой массив там, где текст деградирует с ⚠
  --json у blast/hierarchy/context  проза вместо JSON на ненайденном символе
  --limit                     обрезанное число подано как тотал
  construct                   одна позиция в списке дважды и дважды в счёте

Две правки по ходу: фикстуре C-семейства нужна отдельная цель (SwiftPM не
собирает смешанный Swift+C таргет), а пустой JSON надо разбирать, а не
сравнивать со строкой "[]" — он приходит как "[\n\n]".

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…ений

  не-UTF-8 байт          U+FFFD шире исходного байта, офсеты clang уезжают:
                         тот же код даёт ": [self legacyValue]" против
                         ": legacyValue] * 2; " — сниппет вырезан не там
  нечитаемый .m          в "not scanned" не назван, считается просмотренным
  repo_map через MCP     вызывается без индекса: C-семейства нет, mode: full
  провенанс в MCP        метка устаревания живёт в stderr и до агента не
                         доходит вовсе
  list_implementations   на неизвестном символе isError: false и
                         "no implementations found" — утверждение о символе,
                         которого нет

Три правки по ходу, все — мои ошибки в оснастке, не в инструменте:
index --no-build флаги не захватывает, поэтому clang-слой работал вхолостую;
селектор value неоднозначен в Foundation и паттерн не компилируется;
пустой JSON приходит как "[\n\n]" и сравнивать его со строкой нельзя.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
  потолок 1000        символ с 1200 ссылками отдаётся как "usages: 1000"
                      без признака усечения; --limit его не поднимает
  демон               файл, созданный после старта демона, невидим навсегда:
                      memo файлового списка не сбрасывается, и в ответе нет
                      признака, что он пришёл из демона

Два теста не добавлены, и по разным причинам:

Выбор стора по mtime вместо покрытия — дефект, с которого всё началось, —
воспроизводится вручную (шаги записаны в файле) и кладёт семантику целиком
на реальном Xcode-проекте, но синтетическим тестом воспроизвести не удалось:
правильный стор продолжает выигрывать, и причину я не установил. Тест,
переставший проверять свой дефект, читается как «починено» — это ровно тот
отказ, против которого реестр и заведён.

Раздувание счёта макросами требует пакета с макросом: 81 секунда сборки
swift-syntax, и на минимальной фикстуре не воспроизводится. Проверено
вручную на этом репозитории: refs даёт 47 при 30 вхождениях, а --verify
печатает semantic 47 против textual 31 и молчит.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
hasSourceNewer обходил только *.swift — и в git-ветке, и в обходе
файловой системы. Правка .m, .h, .cpp оставляла метку `fresh`, то есть
ярлык доверия на устаревшем ответе. Для инструмента, объявившего своей
средой Swift ВМЕСТЕ с Objective-C, C и C++, слепой была половина среды.

Теперь берётся тот же набор расширений, что и у остальных слоёв
(IndexDeclarations.clangExtensions), — один список на весь инструмент.

Проверено: правка .m в фикстуре поднимает ⚠ STALE, Swift-правка
по-прежнему ловится.

Тест реестра сам сообщил о закрытии — упал с «ожидаемая проблема не
воспроизвелась». Обёртка withKnownIssue снята, утверждение осталось
сторожем регрессии.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…ше молча не делал

- ответ, ссылающийся на удалённые файлы, говорит об этом: удаление не двигает время,
  поэтому маркер свежести его не видит, и без этой строки два сообщения противоречат
- api показывает extension с публичными членами; фильтр перенесён в summaries, чтобы
  --json и текст отвечали одно и то же
- --project проверяется до диспетчеризации: несуществующий каталог — код 2, а не пустой ответ
- body сверяет имя объявления на строке с запрошенным символом и отказывается, когда файл сдвинулся
- пустой символ отвергается во всех семи командах вместо ответа «ничего не найдено»
- флаги в однодефисном написании отвергаются, кроме отрицательных чисел
- ключи .sextant.json с опечаткой названы вместе со списком известных, а не приняты молча

Каждый дефект закрыт своим тестом из реестра: тест падал с «Known issue was not
recorded» и переведён в обычную регрессионную защиту.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Числа перестали быть числами показанного:
- construct считает позицию один раз, две конструкции на строке — одно место
- заголовок impls/callees считает всё найденное, --limit сокращает список и говорит насколько
- внутренний потолок в 1000 ссылок назван в ответе, а не выдан за полное число
- search выполняет --limit и печатает остаток, total продолжает считать всё

Шаблоны и правила:
- шаблон, из которого парсер только восстановился (несбалансированная скобка), отвергается;
  но шаблон, не разбираемый как Swift, уходит в clang — [self feed] остаётся рабочим
- правила lint, не скомпилировавшиеся ни разу, названы во всех трёх выводах

changed:
- уровень доступа входит в сравниваемую сигнатуру: сужение public видно
- переименование файла прослеживается через git -M, чистый перенос не меняет символов
- файл, который не разбирается, не диффается, а попадает в список непросмотренных

Адресация и охват:
- имя пакета читается из манифеста, а не из первого компонента пути
- api отвергает --package, не совпавший ни с одним пакетом, со списком известных
- исключения, убравшие файлы из обхода, названы в search, lint и api

Реестр: закрыто двенадцать записей, каждая переведена в регрессионную защиту.
Осталось 12 из 24.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
clang:
- смещения считаются по тому же байтовому представлению, которое получил clang:
  байт, не являющийся UTF-8, декодируется в U+FFFD шириной три байта, и срез по
  исходным байтам после него попадал не туда — колонка и текст были неверны,
  но подавались как структурный результат
- файл, который не удалось открыть, попадает в «не просмотрено», а не считается
  проверенным и чистым

MCP:
- ответ несёт тот же ярлык происхождения и свежести, что печатает CLI: клиент
  логирует stderr в лучшем случае, и агент — главный потребитель — не видел его
- repo_map получает индекс, как в CLI, а без индекса называет число не-Swift файлов,
  которых на карте нет
- list_implementations отличает неизвестный символ от символа без реализаций

Шаблон, не разбираемый как Swift, по-прежнему уходит в clang ([self feed] — рабочий
объектно-си шаблон); невалидным он объявляется только когда у clang не было файлов.

Реестр: закрыто шесть записей. Осталось 6 из 24.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
- --json всегда отдаёт JSON: символ, которого нет в индексе, описывается объектом
  {symbol, found: false}, проза уходит в stderr (blast, hierarchy, context, refs)
- --json несёт ту же деградацию, что видит человек: при нуле семантических попаданий
  возвращается объект с textual, degraded и причиной, а не пустой массив, читаемый
  как «ссылок нет». Форма намеренно отличается от массива семантического ответа
- фрагмент, на строке которого больше нет символа, не печатается: позиция остаётся,
  текст удерживается, и число удержанных названо
- демон обновляет список файлов на каждый запрос, а не замораживает проект на всю
  свою жизнь

Реестр из 24 дефектов закрыт полностью. Все тесты переведены в регрессионные
защиты, заголовки наборов описывают это.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…а не инструмент

Раньше тест не воспроизводил дефект, и я записал это как «причину установить не
удалось». Причина в моём коде: createSymbolicLink(at:withDestinationURL:) разрешает
относительный путь назначения относительно текущего каталога процесса, поэтому
.build/index-build/debug указывала на <cwd>/host/debug, соперника на диске не
существовало вовсе, и правильный стор побеждал за отсутствием соперника. Шелловый
ln -s делает ссылку по-настоящему относительной — отсюда расхождение с ручным
воспроизведением.

С корректной ссылкой дефект воспроизводится: doctor называет
.build/index-build/host/debug/index/store — копию без юнита b.swift, — refs Widget
отвечает usages: 0 при одной реальной ссылке, и всё это под меткой fresh и ✅ ready.

Заодно девять guard, которые тихо выходили из теста при неожиданном выводе,
заменены на #require: тест, пропускающий сам себя, неотличим от прошедшего.
Проверено — ни один из девяти холостым не был.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
На целевом проекте у tea два стора Xcode: один собран внутри агентского воркtree
(.claude/worktrees/<имя>), другой — в главном чекауте. Воркtree лежит внутри корня,
поэтому проходил проверку «workspace внутри проекта», а будучи пересобранным позже —
выигрывал по свежести. Дальше фильтр записей отвергал каждую его запись как чужую, и
инструмент отвечал нулём на любой семантический вопрос под меткой fresh и ✅ ready.

Два слоя расходились в одном предикате. Теперь селектор применяет тот же
IndexStore.inScope, что и фильтр: стор, чьи записи будут отвергнуты, не выбирается.
Из самого воркtree его стор по-прежнему выбирается — предикат симметричен.

Замер на tea: refs AbsAccountsViewModel через автовыбор давал 0 семантических
попаданий и объяснял это «замыканием или локальной переменной» — про класс. Теперь
даёт определение и 7 использований в 3 файлах, как явный стор главного чекаута.

Вторая половина — на случай, когда чужой стор всё-таки используется (--index-store,
другая раскладка): пустой ответ называет причину вместо выдуманной. Обе ветки
деградации, и с текстовыми совпадениями и без, говорят «индекс держит N записей на это
имя, все вне проекта — они принадлежат <путь>».

Ранжирование по времени не тронуто: остаётся открытым вопрос, должен ли более полный
стор выигрывать у более свежего. Его тест в реестре остаётся под withKnownIssue.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…ё объясняет

У проекта регулярно несколько индексных сторов, и они отвечают на один вопрос
по-разному. Замер на этом репозитории, refs SwiftSources:

  .build/x86_64-…/debug   525 юнитов, 16 авг → 83 использования в 22 файлах
  .build/index-build/…    560 юнитов,  9 авг → 34 использования в 12 файлах
  объединение обоих                          → 91 использование в 22 файлах

grep даёт 73 вхождения в 22 файлах. Ранжирование по числу юнитов указало бы на
худший ответ, ранжирование по времени — на любой из двух, и в самом ответе
различие невидимо. Поэтому инструмент больше не выбирает сам.

- StorePolicy: recency (один стор, записанный последним) и union (все пригодные,
  с дедупом; свежесть по самому старому). У каждой политики в коде записано, что
  она даёт, чего не даёт и чем рискует — этот же текст печатается человеку
- пока пригодный стор один, политика не нужна и не спрашивается
- как только пригодных больше одного и политика не задана, семантические команды
  отказываются отвечать и печатают сравнение сторов и политик; doctor говорит
  «store policy NOT SET», а не «стор не найден» — индекс есть, не хватает решения;
  MCP отдаёт тот же текст в теле ответа, а не в лог
- sextant store — что в досягаемости и что даёт каждая политика;
  sextant store use <политика> — записывает решение в .sextant.json
- разовое переопределение --store-policy, машинное SEXTANT_STORE_POLICY,
  полный обход --index-store
- при каждом использовании, где выбор был, ответ несёт строку решения: сколько
  кандидатов, какие читаются, какие оставлены и почему
- опечатка в значении настройки отвергается со списком известных, как и опечатка
  в ключе

Отладочный и релизный сторы по-прежнему не смешиваются: в кандидатах остаётся
конфигурация, собранная последней, вторая названа отвергнутой с причиной.

Цена: перечисление кандидатов 0.3–0.5с на tea (список 22 725 юнитов), считается
один раз за команду. ADR-0006 и README.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Число юнитов не про покрытие, и это замер, а не рассуждение: на этом репозитории
стор с 560 юнитами собран из 69 файлов проекта из 143, а стор с 532 юнитами — из 132.
Ровно этим объясняется разница в ответах (34 против 83 использования). Ни время
записи, ни размер стора этого не показывают — нужен сам факт: из каких файлов стор
собран.

Читатель юнитов (Sources/CIndexStoreShim + IndexStoreUnits):
- подмножество ABI libIndexStore объявлено вручную и резолвится через dlsym — тем же
  способом, что уже сделан для libclang: тулчейн не поставляет заголовки, а линковка
  ломала бы запуск там, где тулчейна нет
- на юнит доступны главный файл, объектный файл, таргет, конфигурация, модуль и
  признак системного юнита
- проверено против реальности: главные файлы сверяются с файлами на диске, число
  прочитанных юнитов — с листингом каталога. Ошибка в имени символа
  (get_out_file вместо get_output_file) вскрылась на первом же прогоне

Политика coverage:
- читает юниты каждого кандидата, считает пересечение с файлами проекта на диске и
  берёт покрывающий больше; при равенстве — более свежий
- где libIndexStore недоступна, покрытие не измеряется и стор не проигрывает нулём:
  ранжирование откатывается к recency и говорит об этом. Каталог, не являющийся
  стором, читается как пустой, поэтому пустота не доказательство — закреплено тестом
- покрытие считается только когда пригодных сторов больше одного; при единственном
  сторе решать нечего и ничего не читается. На tea цена запроса не изменилась
  (23–27с до и после). В sextant store покрытие считается всегда — это экран решения
- кэш по (путь стора, время записи юнитов, число файлов проекта): 4.8с на 22 725
  юнитах вхолодную, ~0.5с при попадании

Причина, по которой стор оставлен, теперь написана языком действующей политики:
под coverage — «покрывает меньше (69 файлов против 132)», а не «не самый свежий».

ADR-0006 и README обновлены.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Проверка пяти репозиториев из бенчей (Alamofire, swift-argument-parser,
swift-numerics, swift-nio, swift-syntax) — 146 проверок по всем починенным
дефектам. Два из них оказались починены не до конца.

api --json отдавал внутренние члены публичных типов. Фильтр видимости я применил
только к верхнему уровню, а члены шли как разобраны: текстовый ответ их отбрасывал,
машинный контракт — нет. Один вопрос, два ответа. Найдено на Alamofire, где в
--json попадали внутренние init и вспомогательные методы. Теперь члены фильтруются
тем же правилом рекурсивно; внутренний extension с публичными членами остаётся
частью поверхности — это отдельное правило, и оно не изменилось.

search с неразборным шаблоном отвечал кодом 0, если в проекте есть C-файлы без
флагов компиляции. Я засчитывал непросмотренные файлы за «шаблон куда-то дошёл»,
а непросмотренный файл — ровно наоборот, доказательство того, что его не искали.
Теперь пригодность шаблона доказывает только реально просмотренный файл, а когда
таких нет — называются обе причины сразу, и шаблон, и непросмотренные файлы.
Найдено на swift-numerics (_NumericsShims.h + .c без флагов).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…вился бюджет

Предел в 4000 файлов стоял с первого коммита и был отказом: на проекте больше
предела map, api, search и lint отвечали «файлов больше лимита» и не отвечали
ничего. Вопрос о первых четырёх тысячах файлов получал в ответ пустоту.

Замер на tea (12 351 файл), почему предел вообще нужен:

  search 335с · api 139с и 3.75 МБ · lint >6 мин · map ~5.2 мин · сам отказ 1с

Теперь предел ограничивает обход, а ответ называет непрочитанное:

  ⚠ covered 4000 of 12351 file(s): the walk stops at --max-files 4000.
    The rest were not looked at — narrow with --scope, or raise --max-files.

Обход отсортирован по пути, поэтому предел — это всегда один и тот же префикс:
два запуска покрывают одни и те же файлы, иначе оба называли бы себя «4000 из
12351», покрывая разное. На tea: search 50с, api 35с и 1.1 МБ, map 28с.

У api появился бюджет вывода, как у map, но без умолчания: публичная поверхность —
это контракт, и молча урезанный контракт хуже длинного. С --budget заголовок
говорит, что осталось за бортом:

  # Public API • declarations: 241 • ⚠ truncated: 1384 file(s) with 13778
  declaration(s) left out by --budget 6000 tok

В MCP у бюджета умолчание есть (8000 токенов): ответ читает агент с окном контекста,
и три мегабайта туда не помещаются. Строка о неполноте в MCP идёт в теле ответа —
stderr агент не видит.

exceedsLimit с ранним выходом удалён: он обслуживал только отказ.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…ызова названо отдельно

calls(ofUSR:direction:) клал в location место вхождения, то есть строку, откуда
вызвали. На этом репозитории timestamp(ofStore:) показывался на IndexFreshness.swift:48,
а определён на строке 25 — и в ответе ничто не говорило, что это за позиция.
Уверенно неверная координата в ответе, который для того и нужен, чтобы по нему ходить.

Определение достаётся по USR тем же путём, что уже используется в lookup. RelatedSymbol
получил два поля: callSite (где написан вызов) и definitionKnown. Внешний символ,
которого в индексе нет, не получает выдуманную позицию: он помечен «это место вызова,
определения в индексе нет».

Обе стороны и обе поверхности:
  → callees: timestamp(ofStore:) … IndexFreshness.swift:25 · called at :48
  ← callers: candidates(forProjectRoot:) … DerivedDataLocator.swift:23 · called at :36

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…ся второй раз

--verify молчал ровно в невозможную сторону. Порог «текстовых втрое больше»
срабатывал, а обратный перекос — семантических больше, чем текстовых, — проходил
молча, хотя каждая ссылка в индексе это место, где имя написано, и текстовых не
может быть меньше. На этом репозитории пара была semantic 81, textual 73.

Причина не в пороге, а в счёте. Макрос записывает ссылку дважды: на настоящей
колонке и на позиции самого макроса. Пример из этого репозитория:

  #require(SwiftSources.borrowedGitFiles(...))
  индекс: BorrowedGitTests.swift:37:25 и :37:34, имя написано на 34-й

Дедуп по (файл, строка, колонка) обе оставлял. Теперь позиция, чья колонка не
несёт имени символа, отбрасывается — но только если на той же строке есть
позиция, которая его несёт. Одинокая непроверенная позиция остаётся: имя может
быть написано в форме, которую эта проверка не видит (Self, псевдоним, селектор),
и потерять настоящее место хуже, чем посчитать одно дважды.

Читаются только строки, где позиций больше одной — на большом проекте это почти
ничто: refs на tea 25–27с, как и было.

Замер после правки: semantic 74, textual 74, и grep -o даёт те же 74.

Сам --verify теперь называет и обратный перекос, с причинами, которые могут его
дать (предел файлов, exclude, код, порождённый макросом).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…ется, охват в метке доверия

Три пункта из списка открытых.

Широкий --project брал индекс чужого проекта. Префикс пути — не граница проекта:
на --project ~ подбирались сторы всех проектов, которые пользователь когда-либо
собирал. Замер: предлагался стор постороннего приложения из Downloads, покрывающий
6% того, что называлось «этим проектом». Теперь граница — чекаут: workspace годится,
если он в том же репозитории, что и корень. Вне git — прежняя раскладка: workspace
лежит в самом корне.

Путь DerivedData был захардкожен, поэтому проект, собранный через
xcodebuild -derivedDataPath ./build (а так делает любой CI), выглядел как проект
без индекса. Появились --derived-data и SEXTANT_DERIVED_DATA.

Релизный стор без тестовых целей молча давал меньший ответ. Это частный случай
«стор покрывает не весь проект», и теперь метка доверия несёт охват рядом со
свежестью — свежесть про время, охват про полноту:

  debug:   [… fresh · covers 135/146 files (92%)]   → usages: 74 в 23 файлах
  release: [… STALE · covers  81/146 files (55%)]   → usages: 46 в 17 файлах

Охват кэшируется по состоянию стора, поэтому это чтение кэша на каждом запуске
кроме первого после сборки: refs на tea 22–23с против прежних 25–27с.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…реживают чтение

У флагов C-семейства не было понятия времени вообще. Их снимает sextant index;
любая другая сборка — Xcode, swift build, CI — двигает индекс вперёд и оставляет
флаги позади, и ни один структурный ответ об этом не говорил. Это не обычная
неполнота: -DFEATURE=1, ставший нулём, даёт структурное совпадение внутри кода,
которого в текущей сборке нет. Высший уровень достоверности инструмента стоял на
устаревшем факте.

Время съёмки — это mtime самого файла базы, поэтому формат остался тем же
compile_commands.json, который читают clangd и libclang. Когда сборка новее съёмки,
ответ это называет:

  ⚠ compile flags captured 04:05:44; the project was built 11s later (04:05:55) —
    the C-family answers below stand on the older flags…

Обычный случай — index снимает флаги после сборки — молчит, иначе предупреждение
становится шумом и его перестают читать.

Записи на удалённые файлы отбрасываются при чтении, а не только когда новая съёмка
их перекроет. Замер на этой машине: две базы держали 75 из 75 и 72 из 73 записей на
файлы, удалённые неделями раньше.

Тест круговорота через файл переписан на настоящие файлы: на выдуманных путях он
проверял бы отсев, а не круговорот.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…тора и конфиг на каждый вызов

Недосчёт под частичным #if. Замеренный случай — semantic 1, textual 3 — падал ровно
на неверную сторону порога «текстовых втрое больше» (3 > 3 ложно) и проходил молча.
Порог тут вообще не тот инструмент: у разрыва есть знаемая причина. Теперь, когда
семантических меньше, считается, на скольких строках имя стоит внутри #if-веток,
которых нет в этой сборке, и это называется:

  [cross-check: semantic 1, textual 4]  ⚠ the name appears on 1 line(s) inside `#if`
  branches this build does not contain — the index covers one configuration…

Совпадающие счётчики по-прежнему молчат.

MCP: выбор стора теперь переспрашивается на каждом вызове, которому нужен индекс, а
не только пока индекса нет; при смене подписи сервер переоткрывает индекс и пишет об
этом в журнал. И конфиг больше не читается один раз на процесс — .sextant.json,
изменённый при работающем сервере, вступает в силу со следующего вызова, как уже
сделано для списка файлов.

Честно про доказательность: обе правки верны по построению, но добиться сценария,
который на фикстурах различал бы старое и новое поведение, мне не удалось — прежнее
тоже подхватывало появившийся стор, а мои попытки построить различающий случай не
сошлись. Поэтому измеренного эффекта я не заявляю.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…авлено на обнаружение

Решение владельца: nib'ы не поддерживаем. В 2026 году голый Interface Builder —
редкий способ собирать приложение под Apple, а чтение его означало бы второй парсер,
второе понятие ссылки (строка в XML против записи в индексе) и вторую вещь, которую
надо держать честной.

Записано там, где читатель встретит это раньше, чем удивится:
- README и README.ru — раздел «чего инструмент не читает», рядом с языками;
- роадмап, раздел «не мы», рядом с языками вне Apple-среды;
- ADR-0007 с замером и границей применимости решения.

Замер на tea: 56 storyboard, 270 xib, 376 классов привязано оттуда — и ни одного
класса, который живёт только там. Значит цена — недосчёт при оценке влияния правки,
а не уверенное «этим никто не пользуется». Там, где nib окажется единственным
потребителем, решение стоит пересмотреть: это уже другой класс ошибки.

Отвергнуто предупреждение в ответе при наличии .xib: на tea оно печаталось бы на
каждом семантическом ответе, не меняя ни одного по существу, — а предупреждение,
которое видно всегда, перестают читать вместе с теми, которые важны.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Отставали два раздела, добавленных сегодня, — и это ровно те два, которые меняют
поведение на большом проекте, то есть их отсутствие читалось бы как «у нас этого нет»:

- «Большие проекты: ограниченный ответ, а не отказ» — предел --max-files, строка о
  непрочитанном, префикс в фиксированном порядке, и отдельно про бюджет api;
- «Какой стор отвечает» — три политики, отказ вместо угадывания, охват в метке
  доверия, цена coverage и способы переопределить выбор.

Заодно приведено в соответствие то, что разъехалось: в таблицу команд добавлен store,
а «25 команд» в статусе исправлено на 27 в обеих версиях — в каталоге их столько.

В английском README добавлена пустая строка перед «Which index store answers»:
без неё заголовок склеивался с предыдущим абзацем при рендеринге.

Проверено: заголовки идут один к одному в обоих файлах, таблицы команд по 22 строки,
числа в новых разделах совпадают до единого, ссылки на ADR ведут в существующие файлы.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
RSafargalin and others added 3 commits August 17, 2026 15:09
…енд сохранён

Последний пункт из списка открытых закрыт замером, и замер опроверг ожидание.

Стенд: стор Xcode эталонного проекта, 22 725 юнитов, 528 МБ; refs; время на процесс.

  процессов   одна общая база   по базе на процесс
      1            25.0с              29.5с
      2            25.2с              44.6с
      4            34.4с              73.8с

Раздельные базы (стор скопирован на четыре пути, у каждого процесса свой ключ) хуже
на каждом уровне: четыре базы по 256 МБ вытесняют друг друга из страничного кэша, а
одно общее отображение читают все четыре. То есть общая база — не вред, а причина,
по которой одновременная работа дешёвая.

Помимо скорости проверено: ответ одинаков при 1, 2 и 4 процессах, на холодной базе и
на тёплой; после всего прогона запрос отвечает верно; база осталась 2 файла и 256 МБ.
Ни переимпорта, ни неотпущенной блокировки, ни повреждения.

Один случай стоит денег: CLI, импортирующий холодную базу при живом демоне, — 36.5с
против 24.7с у того же импорта в одиночку. Это один раз после пересборки, ответ верен,
и без ещё одного контроля приписать разницу общей базе, а не двум одновременным
процессам, нельзя. Не проверено: больше четырёх процессов, стор, перезаписываемый
Xcode во время запросов, другая платформа.

Скрипты положены в docs/measurements — чтобы утверждение можно было перепроверить, а
не принимать на слово, и чтобы повторное открытие вопроса начиналось с готового стенда.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Минорная версия, потому что ломается часть контракта стабильности: коды возврата
(неразборный шаблон, пустой символ, однодефисный флаг, несуществующий --project,
несовпавший api --package теперь падают), поведение --max-files (отказ стал
ограниченным ответом, код 1 → 0 на большом проекте) и формы --json (деградация refs
приходит объектом, blast/hierarchy/context на неизвестном символе — объектом вместо
прозы).

По процедуре RELEASING.md:
- Sextant.version → 0.9.0, «version 0.9.x» и V=0.9.0 в обоих README;
- оба changelog'а: [Unreleased] → [0.9.0], 74 пункта в каждом, состав совпадает;
- бенчи перезамерены (сценарии A и B) на тех же закреплённых коммитах — для этого
  клоны пришлось поставить обратно на них, три из пяти успели уехать. Поверхности
  выросли (Alamofire 973 → 1226 объявлений): объявления под #if и внутренние
  extension'ы с публичными членами теперь часть поверхности. Полоса экономии
  79–91% → 79–90%, и в тексте объяснено, почему края полосы именно такие;
- рецепты перепроверены на Alamofire 0455bfb: три блока из двенадцати разошлись, все
  три из-за правок этого релиза — имя пакета из манифеста (## Source → ## Alamofire),
  место определения и место вызова в иерархии, уровень доступа в changed. Обновлены в
  обеих версиях, после чего сверка даёт ноль расхождений в двенадцати блоках.

Что осталось человеку: тег v0.9.0 и его push (это запускает release.yml), а после
выхода архива — обновление Formula/sextant.rb значениями из release notes. Формулу
сейчас тронуть нельзя: sha256 считается по архиву, которого ещё нет.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
CI падал на этом дважды — и на недельной версии, и на нашей: тест «Draining does not
block the writer on more than a pipe buffer» не укладывался в минуту на раннере,
проходя локально за 0.002с. Предположение «мы это уже починили» проверено пушем и
опровергнуто: 317 тестов, одна проблема, ровно этот тест, 131с.

Причина не в тесте. Вычитывание уходило блоком в DispatchQueue.global(), а запись шла
на текущем потоке. Очередь обещает выполнить блок когда-нибудь, и этого недостаточно,
когда вызывающий уже стоит на записи: пул конечен, и всё, что держит его потоки,
лишает читателя потока — а именно он должен разблокировать запись. В наборе таких
держателей стало больше, потому что за эту сессию добавились наборы, ждущие
swift build через waitUntilExit, поэтому и время выросло с 76с до 131с.

Воспроизведено локально: 80 блокированных задач в пуле, запись 500 КБ не вернулась
вовсе, и сторож, поставленный на ту же очередь, тоже не запустился — поэтому прежний
код и не мог сообщить о собственном зависании.

Теперь у каждого читателя свой Thread. В тех же условиях: 500 000 из 500 000 байт за
0.00с. Голодовка пула закреплена тестом, который её создаёт и сразу отпускает.

Это не только про CI: тем же механизмом демон захватывает вывод каждого запроса, то
есть команда с выводом больше буфера пайпа могла подвесить демон на занятом пуле.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@RSafargalin RSafargalin changed the title Measure before building: close three plan items, and say what index executes 0.9.0 — an answer that says what it stands on: store choice, honest counts, bounded answers Aug 17, 2026
@RSafargalin
RSafargalin merged commit 47325c6 into main Aug 17, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant