Тип: Живой бэклог реализации. Рамка: DESIGN.md (дизайн v1 закрыт, решения §10 приняты).
Статус: ✅ M0 + M1 + M1.5 + M4 + M5 + M2 + M6 + M7 + M8–M18 сделаны (M8–M12 — 2026-07-29,
findings глубокой обкатки F8/F4/F3/F7/F6; M13 — serve-эргономика F9–F13; M14 — 2026-07-30, soundness B1
F14/F15, схема 0.9; M15 — diff/change-review A11 F16/F17, codemap review; M16 — архитектура A9 F18–F21,
report architecture; M17 — MCP-адаптер serve --mcp; F22 — компактный MCP-payload; M18 —
возраст графа в stats + codemap refresh) + M3.1 тёплый serve-режим. Режим: use-driven —
codemap вынесен в отдельный репо и подключён к живому ИИ-агенту через MCP; оси добора (A10/A12/B2) —
watchlist «по нужде» (см. gaps/dogfood_axes.md). Отложено — M3.2 полный watcher / M3.3 SQLite /
двух-графовый diff (added/deleted) — брать при нужде/масштабе. 🟢 R1 — исследовательский трэк
(ландшафт соседних тулов, research/) открыт 2026-08-02.
Вехи от «тонкого сквозного среза» к расширению. Внутри вехи задачи упорядочены по зависимости.
Отсылки §N — разделы DESIGN.md.
Цель: codemap build <path> → graph.json → markdown-отчёт API-поверхности, на bquant.
Самое маленькое сквозь весь конвейер Extract→Build→Store→Serve (§8).
- M0.1 Каркас пакета —
codemap/codemap/(§9),pyproject.toml, зависимостьgriffe,codemap/tests/,.gitignore, свой.venv(uv). Вне wheelbquant(whitelist). - M0.2 Extract (griffe-адаптер) —
extract/griffe_extractor.py: статический разбор (без импорта), модули/классы/функции/атрибуты, сигнатуры, докстринги, публичность (__all__/_), депрекация по декоратору@deprecated(§3, §10.2). Алиасы/импорт-рёбра — M1. - M0.3 Модель —
model.py: нейтральныеNode/Edge/Graph, открытые kind, детерминированныйto_dict(сортировка, без таймстампов;codemap_schema) (§2/§2.2). - M0.4 Store —
store.py: каноническийgraph.json(JSON, диффабельный). - M0.5 Report (вид D) —
serve/api_surface.py: markdown — публичные символы по модулям + сигнатуры + докстринги + маркер deprecated (§4.1-D). - M0.6 CLI —
cli.py:codemap build <path>,codemap report api-surface; JSON по умолчанию, exit-коды (§6, §14.1). - M0.7 Тесты на bquant — 6 тестов зелены: 1709 узлов;
analyze_zonesверная сигнатура;MACDZoneAnalyzerdeprecated; детерминизм; roundtrip; отчёт корректен.
DoD: ✅ на bquant end-to-end даёт корректный детерминированный API-surface отчёт.
Результат: 1709 узлов (89 mod / 126 cls / 863 fn / 631 attr), 6/6 тестов.
Цель: от одного отчёта — к графу, отвечающему на каталог §1.
- M1.1 Рёбра импортов — griffe резолвит относительные (5-точечные) в абсолютные; экстрактор
сводит к module→module
imports-рёбрам (241 на bquant), внешние отсекаются (§3.1). - M1.2 Идентичность + export-рёбра — все внутренние ре-экспорты как
export-рёбра с флагомpublic(кейсanalyze_zonesчерезzones/__init__— ре-экспортирован, но не в__all__) (§2.1). - M1.3 Query-API +
codemap query—query.py(networkx):find/where_defined(резолв ре-экспортов) /dependencies/dependents; CLIcodemap query <name>(§4). - M1.4 Reports C —
serve/audit.py:report dependencies(циклы + топ-зависимые),report dead-code(сироты, с оговоркой про эвристику) (§1-C).
DoD: ✅ отвечает на каталог §1 для bquant; резолв импортов/ре-экспортов корректен. 13/13 тестов.
Находки codemap о bquant: цикл pipeline↔cache; analyze_zones вне __all__ пакета zones.
Гэп-док (полнота покрытия, семантика, data flow): gaps/coverage_gap_analysis_2026-07-24.md (CM-01…CM-14; реестр проекта G13).
Цель: устранить расхождение дизайн↔код — эмитить рёбра, обещанные в §2 как v1, но
пропущенные в M0/M1; обогатить узлы данными, которые griffe уже даёт. Схема → 0.2.
- M1.5.1
inherits(CM-08) — класс → базовый класс; griffe резолвит базу в канон-путь; внешние базы (abc.ABC) помечаютсяextras.external. 52 ребра на bquant (41 внутр / 11 внешн). - M1.5.2
decorated_by(CM-06) — символ → путь декоратора; 165 рёбер; запросdecorated_with('deprecated'/'register')(§2). - M1.5.3 Типы полей + dataclass (CM-01/02) —
extras.annotationдля атрибутов (List[ZoneInfo]),extras.is_dataclassдля классов. - M1.5.4 Динамическая регистрация (CM-07) —
@Registry.register('key')→extras.registry{decorator, key}, wiring реестра стал queryable (§7). - M1.5.5 Query + CLI —
bases/subclasses/decorated_with;codemap queryдля класса выводит иерархию. 10 тестов.
DoD: ✅ CM-01/02/06/07/08 закрыты; 23/23 теста; детерминизм держится; схема 0.2.
Остаются отложенными (по дизайну §7): CM-09 call-graph, CM-10 data-flow, CM-11 локали.
Цель: поверх одного графа (структура M0/M1/M1.5 + поведение M4/M5) — читаемые виды
для ИИ и человека. Только serve/, без нового извлечения. CLI-глагол export.
- M2.1 RAG-экспорт —
serve/rag.py: чанк на символ (id+сигнатура+докстринг+место+ соседиcalls/called_by/bases/subclasses/returns/registered_as) + полеtextдля эмбеддинга; JSONL. 989 чанков на bquant.codemap export rag(§1-A, §4). - M2.2 Obsidian-vault (B) —
serve/vault.py: заметка на модуль/символ +[[wikilinks]](внутренние цели), теги#class/#function/#deprecated, index. 1079 заметок.codemap export vault -o <dir>(§4.1-B). - M2.3 Скоупленные подграфы + mermaid —
serve/mermaid.py:class(classDiagram изinherits),deps(изimports),calls(BFS от root поcalls);--scope/--root/--depth(§4.2, §4.1-A).codemap export mermaid --mkind ....
DoD: ✅ три вида (RAG/vault/mermaid) поверх одного графа; скоуп по префиксу/root+depth; 46/46 тестов. Питаются данными M4/M5 (соседи по вызовам, классовая иерархия).
Цель: закрыть до достаточной границы главную семантику — вызовы и потоки данных
(gap-док CM-09/10/11). Схема → 0.3. Граница задана спайком (2026-07-26): чистое
разрешение вызовов по именам = ~18-19% call-site'ов; остальное — вызовы на локальных
переменных (нужен вывод типов локалей → паркуется отдельным тиром) + builtins/external
(помечаются, не гонимся). Принцип: разрешил или честно пометил — тул сообщает свой % сам.
- M4.1 Type-flow (сильный дешёвый слой) — структурные
params/returnsв extras функций (попутно CM-03); Queryproducers/consumersпо имени типа. Отвечает «что порождает/естDataFrame/ZoneAnalysisResult» без разрешения локалей (§7, CM-10 на уровне типов). - M4.2 Call-graph best-effort —
extract/behavior.py: отдельныйast-проход (плагин),calls-рёбра caller→callee с меткойresolution(module/self/imported); внешние/builtin/ локали — не рёбра, а счётчики. 933 ребра на bquant. Querycallers/callees(CM-09). - M4.3 Control-скелет —
extras.controlна функции (ветвления/циклы/try/generator/async) (CM-11-lite). - M4.4 Symbol-level dead-code —
dead_symbols(): приватные функции без входящих resolved-вызовов (сильный сигнал); отчётreport behavior+ апгрейдreport dead-code(CM-12). - M4.5 Честность —
report behaviorпечатает % разрешения; каждое ребро — сresolution; dead-code с дисклеймером «кандидаты, не доказательство».
DoD: ✅ вызовы/потоки покрыты до заявленной границы; 33/33 теста; детерминизм; схема 0.3.
Цель: снять хвост, запаркованный в M4 — вызовы на локальных переменных
(x = Foo(); x.bar()), где живёт кросс-объектный поток pipeline. Решение — замерено
спайком, не заявлено (см. gaps/call_resolution_spike_2026-07-26.md): jedi даёт
реальный подъём (self .foo() → 99%, хвост локалей → +27%), но упирается в ~28-30%
(остаток неразрешим статически — это Python, не лень).
- M5.1 jedi в зависимости —
jedi>=0.19; ленивый импорт (fast-путь её не тянет). - M5.2 Deep-резолвер —
behavior.py:jedi.Script.gotoна каждый call-site, резолв в определение bquant; меткаresolution="deep". griffe остаётся на структуру, jedi — только на вызовы (разделение труда, не замена). - M5.3 Два тира —
extract(deep=False|True), CLI--deep. fast (ast, <1с, дефолт, детерминизм/CI) и deep (jedi, ~50с, богатый граф). Оба детерминированы. - M5.4 Тесты — синтетический фикстур
tests/fixtures/deeppkg(быстро, детерминированно): deep кракаетe.run()на локали, fast — нет; self-вызовы; deep ⊃ fast; детерминизм.
DoD: ✅ на bquant fast 18.6% → deep 25.7% (+359 рёбер), сборка 49.7с; 38/38 тестов; детерминизм в обоих тирах. Граница v1 (осознанно НЕ берём): sound call-graph, value-level data-flow, межфункциональный points-to (§7) — неразрешимо/непропорционально.
Цель: снять доминирующий разрыв обкатки (F1, gaps/observability_dogfood_2026-07-28.md) —
codemap видел только пакет bquant/, а на вопрос «кто использует / можно ли удалить X» blast
radius живёт в тестах/доках/examples/scripts. Расширили область до мульти-рута в пределах
одного репо (§10.12), сохранив нейтральное ядро. Ценность — не «ещё файлов», а транзитивный
типизированный impact-анализ, которого grep не даёт. Схема → 0.4 (аддитивно). Оба режима
настоящие — прогнаны и сравнены эмпирически (обкатка-2 на том же MACDZoneAnalyzer).
- M6.1 Роуты + провенанс —
extract_repo(core, consumers=, docs=, mode=, deep=)(extract/roots.py); каждый узел помеченextras.root(core/tests/examples/research/scripts/docs). CLI-флаги--consumer/--docs(repeatable). Ядро остаётся на griffe; потребители (не-пакеты, россыпь.py) — ast-скан ссылок в ядро с резолвом ре-экспортов ядра. - M6.2 Режим thin — потребитель = один
module-узел; его использования символов ядра →calls/references-рёбра от файла. Дёшево. bquant: 1872 узла / 5049 рёбер / 2.8с. - M6.3 Режим full — функции/классы потребителя материализуются (
contains), ребро- использование исходит из объемлющей функции. Флаг--mode(дефолт thin). bquant: 3210 / 7139 / 3.2с. - M6.4 Doc-слой — скан
*.md(регэкспы по имени ядра) наfrom core… import X+ точечныеcore.a.b.C→doc-узлы +references-рёбра. Точный (from-import + exact node), прозаические упоминания голым именем не ловятся (осознанный lower bound: 7/11 doc-файлов на MACD). - M6.5 Inbound-refs в query (F2) —
query <symbol>печатаетused by → root: N(сводка входящих по роутам);Query.references_to()спанит все impact-рёбра. - M6.6 Impact-вид —
report impact --symbol X(serve/impact.py): транзитивный blast radius (Query.impact, distance 1 + дальше по inbound), разбивка по роутам и типу ребра, дисклеймер «lower bound». - M6.7 Тесты + обкатка-2 — фикстур
tests/fixtures/reporoot(ядро+потребитель+doc, ре-экспорт); 11 тестов; на bquantMACDZoneAnalyzerотдаёт полный blast radius (core 2 / docs 7 / examples 1 / scripts 2 / tests 19 — точно совпало с grep-списком), thin/full сравнены.
DoD: ✅ на bquant impact-запрос по MACDZoneAnalyzer возвращает backward-compat тест-набор
(19 файлов / 55 функций в full) + doc-ссылки, сгруппированные по роуту и типу ребра, транзитивно;
thin/full сравнены; детерминизм держится; схема 0.4; 57/57 тестов (6 M0 + 7 M1 + 10 M1.5 +
8 M2 + 10 M4 + 5 M5 + 11 M6). Findings F3 (class-neighbors в RAG) и F4 (registry-map вид) —
кандидаты, не блокеры (открыты).
Цель: снять F5 (gaps/dispatch_bridging_2026-07-28.md) — навигационная обкатка показала,
что цепочка вызовов от analyze_zones рвётся на плагин-швах (детекция зон + стратегии фич):
self.x = create_swing_strategy(name) → self.x.calculate(...) уходит в unresolved. Данные для
моста есть (реестр M1.5: 12 привязок key→class + Protocol-базы) — не хватало ребра call-site→таблица.
Схема → 0.5. Ценность — навигация «что реально исполняется», не переизобретение grep.
- M7.1 Таблица семейств —
extract/dispatch.py: группировка зарегистрированных классов по регистратору (register_swing_strategy→swing;ZoneDetectionRegistry.register→по классу). Конкретные стратегии Protocol не наследуют → группируем по реестру, не по inherits. - M7.2 Атрибут-класса ↔ семейство — скан
self.attr = create_X(...)/Registry.get_X(...)в методах класса → привязкаattrк семейству (паттерн bquant: bind в__init__, вызов в другом методе). - M7.3 Мост метода —
self.attr.method(...)→calls-рёбра к{impl}.methodвсех реализаций семейства,resolution="registry-candidate"(честная over-approximation). - M7.4 Прямой вызов + точный ключ — фабрика/геттер → классы-реализации; строковый литерал
ключа → единственное точное ребро
resolution="registry". - M7.5 Честность — рёбра типа
calls(цепочка/impact/callers подхватывают сразу);report behaviorсчитает мосты отдельно (exact/candidate); счётчик покрытия не трогаем. - M7.6 Тесты — фикстур
tests/fixtures/dispatchpkg(реестр+фабрика+self-attr, без общей базы); 6 тестов: self-attr→оба impl, литерал→точно, callers через мост, детерминизм, bquant-цепочка достаёт стратегии.
DoD: ✅ цепочка от analyze_zones 38 узлов/глубина 3 → 116/7; _detect_zones→5 детекторов,
extract_zone_features→compute-методы стратегий; 44 моста (0 exact/44 candidate); 63/63 теста;
детерминизм; схема 0.5. Граница: точную ветку (какой ключ из config) не берём — семейство-
кандидатов, не одиночку; мост опирается на data-driven соглашение реестра/фабрики.
- M3.1 Тёплый serve-режим ✅ (2026-07-29) —
serve/session.py(Session: граф в памяти,handle({op,args})→{ok,result}, диспетч в существующие сервисы; ops: ping/stats/query/impact/ column(s)/callers/callees/implementers/family/call_contract/report/export) +serve/server.py(построчный JSON-стдио-цикл, устойчив к битой строке/плохим args) + CLIcodemap serve. Транспорт-нейтрально: MCP-адаптер — тонкая обёртка над тем жеhandle(когда понадобитсяmcp-dep). Досье-функцияbuild_query_resultвынесена и переиспользована вcodemap query. +8 тестов (91/91). - M3.2 Свежесть ✅ (2026-08-28) — все четыре кирпича на месте: авто-петля закрыта.
Хронология: M18 (2026-07-30) — возраст графа (
stats.freshnessиз mtime) + сайдкар-рецепт ре-билда; R1-C9 (2026-08-23) — быстрый инкремент (build --incremental, deep ~5s vs ~60s); issue #3 (2026-08-23, коммитcd93dc2) — свежесть стала честной (freshness()описывает обслуживаемый снимок и помечаетstale: true+on_disk_built_at+ причину, когда файл на диске обогнал сервер — конец тихого «fresh» поверх старого графа) + новая операцияreload(serve-op и MCP-tool: подхват пересобранногоgraph.jsonбез рестарта). Итого ручная связкаbuild --incremental+reloadуже закрывает потребность честно. Переоценка вверх (2026-08-28, R2/CodeGraph): критерий «ждём живого сценария» держался на том, что цена петли неизвестна. Теперь известна — у пира она измерена: нативные файловые события → адаптивный debounce → инкрементальная досборка. Перемерено честно (второй проход того же дня): их end-to-end save→ответ — 327–424 мс (не 121 мс: это была ручнаяsyncбез петли), наш — 8.1–8.7 с, то есть разрыв ~20–25×, а не ~70×, как получалось при сравнении нашего end-to-end с их ручной пересборкой. Четвёртый кирпич (2026-08-28,codemap/watch.py+tests/test_m32_watch.py, 17 тестов): две команды, склеенные шеллом, а не фреймворком —codemap watch <path> -o g.json(исходники → артефакт, инкрементальная пересборка) иcodemap serve --graph g.json --watch(артефакт → память,reloadбез рестарта). Один state-machineDebouncedPollerна обе половины, часы инъектируются → тесты ничего не ждут. Замерено: save → ответ на дефолтах (poll 1 с, debounce 2 с + 0.5 с) — 8.1–8.7 с на реальном 90-файловом пакете, из них 4.3 с сама пересборка (9 модулей; холодная сборка 6.8 с): на реальном дереве узкое место — пересборка, а не поллинг. На игрушечном дереве ~4–5 с (почти весь бюджет — debounce) и 1.11 с на--interval 0.3 --debounce 0.3; цена поллинга — один полныйresolve_scopeза интервал, медиана 50 мс на 292 файла / 4.7 МБ (~5% ядра на дефолте). Решения, а не только glue: (1) polling, не inotify — нативные события стоили бы зависимости, цена названа и измерена, а не спрятана; (2) что считать изменением решаетscope_id(тот же манифест, что пишет билд в сайдкар) — контент-хэши, не mtime, поэтомуtouchне пересборка, а откат к тем же байтам — тоже нет; (3) пересборка вне резидентного сервера: экстракция внутри конкурировала бы с запросами, ради которых он живёт, а падение сборки унесло бы сервер; (4) старт догоняет — если записанный в сайдкареscope_idразошёлся с деревом, пересборка сразу, а нечитаемый сайдкар считается устаревшим («не могу сказать» ≠ «всё в порядке», урок R1-C27); (5) сломанное дерево получает честный граф, а не старый — синтаксическая ошибка пересобирается как обычно, символы модуля выпадают, R1-C21-диагностика говорит «missing, not absent»; придержать старый граф было бы добрее на вид и было бы ложью; (6) неудавшееся действие не двигает baseline — полуписаный файл приreloadретраится, а не записывается как успех (иначе сервер тихо отвечает из старого графа, считая себя свежим — ровно та несвежесть, которую убрал issue #3); неудача сборки печатается один раз на версию дерева, чтобы сломанное дерево не спамило. Дефолт debounce разный у половин (2.0 против 0.5) намеренно: дерево правят пачками, артефакт пишется один раз. Док:docs/incremental.md§«The automatic loop». Hash-свежесть строится на M19.A (сверка контент-хэшей с манифестом — точнее mtime). - M3.2-f1 адаптивный debounce ✅ (XS) — (2026-08-28, второй проход по CodeGraph). Плоское окно
2 с облагает налогом обычный случай (сохранили один файл) ради пачки, которая случается редко.
У пира это разведено:
QUICK_SYNC_MAX_PENDING = 2/QUICK_SYNC_QUIET_MS = 300— одиночное сохранение синкается через 300 мс, аgit checkoutпо-прежнему коалесцируется на полном окне (src/sync/watcher.ts). Именно поэтому их измеренные 0.33 с, а не 2 с, как следует из заголовочного дебаунса. Scope: вDebouncedPoller— порог «сколько всего изменилось» и короткое тихое окно; для source-половины «сколько» берётся изdiff_scopes(уже считаем), для artifact-половины порога не нужно (файл один). Приёмка: одиночная правка отвечает быстрее полного окна, пачка из >N файлов по-прежнему даёт одну пересборку; тесты на инъектированных часах, как вtest_m32_watch.py. Сделано:DebouncedPollerпринимаетquick_debounce+size(baseline, pending); окно выбирается один раз, когда изменение впервые замечено.ScopeProbeумеет мерить размер черезdiff_scopes— ту же сверку, что делает билд, а не вторую собственную. Размер неизвестен (baseline старше процесса) или больше двух файлов → полное окно: быстрый путь берётся, только когда про изменение известно, что оно маленькое; ошибка в счёте размера не ломает петлю. Граница — 2 файла, потому что «модуль и его тест» это одно человеческое действие (та же граница у пира). Про эффект честно: детерминированная часть проверяется тестами на инъектированных часах (одно сохранение отрабатывает через тик после обнаружения вместо трёх). End-to-end на этой машине меряться отказывается: разброс полной сборки ±30% между прогонами перекрывает выигрыш, поэтому цифры «стало 6.5 с» здесь нет — есть изменение окна с 2.0 до 0.3 с для ≤2 файлов и тесты, которые это фиксируют. Карточка:research/tools/codegraph.md. - M18 — возраст графа + удобный ре-билд ✅ (2026-07-30, без схемы) — MCP отдаёт статичный
graph.json; теперьstatsнесётfreshness(built_at/age_secondsиз mtime — агент видит, что карта могла устареть) + рецепт ре-билда. Канонический граф остаётся без таймстампов (детерминизм): метаданные живут в сайдкаре<graph>.meta.json(argv/cwd/target/built_at), пишется приbuild -o. Новая командаcodemap refresh <graph.json>переигрывает записанный build.serveпрокидываетgraph_pathв Session.codemap/freshness.py; сайдкар в .gitignore. +8 тестов. - M3.3 SQLite query-бэкенд — индексы (§4). Отложено: networkx-бэкенд держит текущий масштаб (3k узлов); SQLite оправдан только при бóльшем графе/serve-нагрузке. Двери открыты за той же query-поверхностью.
- M17 — MCP-адаптер ✅ (2026-07-30) — тонкая обёртка
Session.handleв MCP-tools (18 tools, по одному на agent-facing op), нативный вызов из AI-агента.serve/mcp_server.py:build_mcp_server(session)(mcp 2.0MCPServer, lazy-import), каждый tool зовётhandleи возвращает конверт (сигналresolved.ambiguousF14 сохраняется). CLIcodemap serve --mcp(stdio).mcp— опциональная зависимость (pip install codemap[mcp]; extra в pyproject), import ленивый — codemap работает без неё. Логики нет — только маппинг. +7 тестов (importorskip mcp).
Рамка (от пользователя): ядро доведено до естественной точки (вынесено в отдельный репо,
подключено к живому ИИ-агенту через MCP, findings F22/M18/F23 закрыты) → открываем
исследовательский трэк. Изучаем соседние тулы анализа кодовой базы и по каждому решаем,
как codemap должен к нему относиться: прямая интеграция / тонкая обёртка-адаптер /
только референс (учимся, не тащим). Формат — набор markdown-отчётов в research/
(один на тул или тему). Находки возвращаются сюда и в gaps/dogfood_axes.md как конкретные
способности, а не спекулятивные фичи.
Оси сравнения (позиционирование codemap): source-only (без сборки/рантайма) · детерминированный канонический граф · CLI-AI-first (JSON по умолчанию) · Python-focus · граф-модель (узлы/рёбра
- provenance) · warm-serve/MCP.
-
R1.0 Ландшафт ✅ —
research/00_landscape.md: категории тулов, где сидит codemap по осям, сводная матрица + вердикт integrate/wrap/learn по каждому. -
R1.1 AI-context / repo-map ✅ — aider repo-map, Cursor/Continue/Cody codebase-index — прямые «конкуренты» по AI-first-использованию (учимся + дифференцируемся).
-
R1.2 Code-graph / semantic-index инфра ✅ — SCIP/LSIF, Kythe, Glean, Stack Graphs, Sourcegraph, ctags — интерчейндж-форматы и схемы графа (кандидаты в адаптер/экспорт).
-
R1.3 Query / dataflow движки ✅ — CodeQL, Semgrep, ast-grep, tree-sitter, Comby — поверхность запросов и бэкенды экстракции.
-
R1.4 Python graph/arch пиры ✅ — pydeps, pyan, code2flow, grimp/import-linter, snakefood, rope — прямые референс-пиры (что делают лучше/хуже, чему учимся).
-
R1.5 Курируемые источники (field intake) ✅ (2026-08-02) —
research/05_curated_sources.md: обработан ТГ-дамп (каналы @ai_for_dev/DevHub/@data_analysis_ml) как bottom-up дополнение к R1. Сырьё вне git (assets/в .gitignore). Итог: поле сошлось на тезисе codemap («граф кода через MCP бьёт grep») — валидация + переполненность ниши. Добавляет живой ростер конкурентов (graphlens, CodeGraph, GitNexus, OntoIndex, Sentrux, cocoindex, rag_for_git, CodeWiki, …) и бенчмарк-доказательство (936 прогонов на superset: граф в 10–23× дешевле grep на impact-запросах). -
R1.0-f1 сверка карты с измерением ✅ (S) — (2026-09-12). Август писался до того, как хоть один пир был установлен; с тех пор шесть карточек hands-on. В
research/00_landscape.mdдобавлена датированная сверка: девять из десяти «кандидатов в способности» уже выпущены (остался только rope — дверь, которая ломает read-only); пять реально конкурирующих тулов в матрице отсутствуют (пришли из R1.5 и R2) и внесены с измеренными вердиктами; дифференциатор №1 («ни у кого нет детерминированного диффабельного артефакта») из аргумента стал замером — пять независимых реализаций, пять недиффабельных артефактов, при том что наш детерминизм за тот же период пришлось сузить до быстрого тира; названо и то, что измерение отняло — «эпистемическую честность» больше нельзя присваивать себе одним (у OntoIndex градуированная уверенность на ребре), а наш recall был 2.4 % и подавался как свойство. Главный вывод трэка записан отдельно: три собственных дефекта (R1-C28, R1-C39, R1-C40) найдены приёмом «померить чужое и задать тот же вопрос себе» — это дало больше, чем список из десяти кандидатов.
Принцип трэка: не строить фичи из отчётов сразу — каждая находка проходит через backlog как конкретная способность и берётся по нужде (use-driven), как остальные оси.
Галочки R1.0–R1.4 простояли снятыми с 2026-08-02 до 2026-09-12 — при том что строка статуса
прямо под ними говорила «отчёты написаны». Два раза за эту неделю ландшафт предлагался владельцу как
«следующая работа» по снятой галочке, а не по тексту. Маркер живёт отдельно от факта, и сверить их нечем:
docs/ держат четыре слоя проверок, а BACKLOG.md — ни одного, и машинного сторожа на «галочка против
абзаца» придумать нельзя, не начав врать про его силу. Поэтому правило, а не сторож: маркер
переворачивается в том же коммите, что и факт. Первое, что сделано в рамках «добора R1.0», — перевёрнут
маркер.
Статус R1.0–R1.4: ✅ отчёты написаны (2026-08-02) — research/00_landscape.md (карта + матрица +
консолидированные вердикты) + 4 тематических (01_ai_context_repomap / 02_codegraph_index_infra /
03_query_dataflow_engines / 04_python_graph_arch_peers). Метод: 4 grounded-агента (web-verified),
по каждому тулу вердикт integrate/wrap/learn.
Ключевые структурные сигналы (для позиционирования): (1) фронтир AI-context дрейфует К тезису codemap — Cody уходит от эмбеддингов к search+graph, Anthropic: agentic grep бьёт RAG; (2) вся граф-инфра сходится на примитивах codemap (Kythe VName / SCIP descriptor / LSIF moniker = каноничный устойчивый id — у codemap уже есть); (3) два source-only-графовых прецедента размечают полосу: stack-graphs заархивирован 2025 под весом рукописных per-language DSL, ctags живёт простотой → оставаться source-only+детерминированным, но никогда не строить собственный name-resolution движок; делегировать jedi/griffe, фокус на Python.
Каждый пункт — готовая к взятию задача (Scope / Зачем / Приёмка / Оценка). Оценка в t-shirt: S≈полдня, M≈1–2 дня, L≈неделя, XL≈крупная веха. Дефолт стойки сохраняется: source-only, детерминизм, read-only, Python-focus — если задача его нарушает, это отмечено.
-
R1-C1 SCIP-экспорт ✅ (2026-08-02, без схемы) —
codemap export scip -o index.scip.codemap/serve/scip.py(build_scip/write_scip) + вендоренные bindings_scip_pb2.py(сгенерены из офиц.scip.proto, guard ослаблен до 5.26), extracodemap[scip]=protobuf(lazy). Честный scope: граф symbol-level (нет координат call-site) → экспортируем defs + SymbolInformation (по одной Definition-occurrence на узел с локацией, kind, docstring,inherits/implements→ SCIPrelationshipsis_implementation); reference-occurrences (find-references) намеренно не льём (нет позиций токенов — фейк хуже пропуска). Symbol-string из каноничных id по грамматике дескрипторов SCIP (namespace/, type#, method()., term.). Детерминированные байты; проверено round-trip'ом protobuf и реальнымscip print. Частично закрывает R1-C7 (структурные descriptor-id доказаны). +8 тестов (importorskip protobuf; CLI-тест skip еслиscipне на PATH). На bquant: 206 documents, 1826 symbols. -
R1-C2 ctags-экспорт ✅ (2026-08-16, без схемы) —
codemap export ctags [-o tags].codemap/serve/ctags.py(build_ctags) — из def-узлов (class/function/attribute) строки extended- формата universal-ctags:name\tfile\t{address};"\t{kind}\t{ext-поля}. Адрес:/^<строка>$/-паттерн когда исходник читаем (устойчив к сдвигу строк — весь смысл ctags-паттернов; экранируются\///$), иначе бэкоф на голый номер строки (всегда есть из графа). Ext-поля (всё из уже имеющихся данных, без догадок):kind(c/f/m/v),line:,scope(class:Foo, module-scope опущен как в u-ctags),signature:(…)+typeref:typename:…(функции, paren-balanced разбор),access:(public/private из visibility),end:. Честный scope: только дефы (не references — это SCIP); модули пропущены (=файлы, как u-ctags); ре-экспорт-алиасы (узел без своей локации) пропущены → каждый деф тегируется один раз в реальном месте. Детерминизм: pseudo-tags объявляют!_TAG_FILE_SORTED\t1, реальные теги сортированы по имени→файлу→адресу (binary-searchable). CLI:export ctags+--project-rootкак source-root. Приёмка выполнена: на bquant@graph 1585 тегов (все classes/functions/methods/attributes с локацией; core), байт-идентичен между прогонами, структурно валиден + сортирован (readtags binary-search находитanalyze_zones). Докиdocs/export.md(секция + таблица). +9 тестов (readtags-CLI-тест skip если не на PATH). Закрывает «пол» способностей из R1-ландшафта. -
R1-C3 Архитектурные контракты +
check✅ (2026-08-14, без схемы).codemap/arch.py(декларативный контракт[architecture]вcodemap.toml: layers ordered / independent / forbidden / no_cycles / exhaustive; парсер толерантен как gate — битый toml → пустой контракт) +codemap check(CLI-гейт: exit 2 на нарушении со списком нарушающих import-рёбер, exit 0 на чистом,--require-contractделает отсутствие контракта провалом) + serve-opcheck+ MCP-toolcheck(паттерн «что я сломал» — агент спрашивает после правки). Приёмка выполнена: на bquant layers-контракт ловит реальное нарушение (indicators.macd → analysis.zones.modelsвверх + циклpipeline↔cache, exit 2) и проходит на forbidden-контракте (exit 0); exhaustive падает на незадекларированном слое (тест). Dogfood: codemap описал собственный слой-контракт вcodemap.tomlиcodemap check --build ./codemapзелёный (тестtest_r1c3_dogfood.pyстережёт). Докиdocs/architecture-contracts.md. Отложено из R1.5-словаря: naming/file-size-правила, health-delta до/после (кандидаты в R1-C4/новый пункт). +18 тестов. -
R1-C4 Метрики сложности в hotspots ✅ (2026-08-16, схема 0.10). Считаются в поведенческом ast-проходе (
extract/behavior._complexity, тот же проход, что control-скелет — source-only, stdlib-only, без radon, детерминированно):extras.complexityна function-узлах несёт cc (McCabe цикломатическая: if/elif/тернар/for/while/except/bool-join/comprehension-clause+filters/ match-case, по собственному телу — вложенные def не двоятся), volume (Halstead: операторы- символы AST + имена/константы), sloc (физический span), mi (Maintainability Index, radon-нормализованный 0–100, ln-входы прижаты к 1). Вплетено в обе оси:Query.hotspots→ god-классы аннотированыtotal_cc/max_cc+ новый списокcomplex_functions(топ по CC, порогmin_cc);report architectureиreport behaviorрендерят; per-symbol cc/mi/volume/sloc в query-досье (build_query_result). Приёмка выполнена: на bquant_create_plotly_zones_on_priceCC 66/MI 12.5,extract_zone_featuresCC 59/MI 9.2; и разрешает коллизию «одинаковое число методов ≠ одинаковая сложность» —NotebookSimulator(23 метода, ΣCC 50) vsStatisticalPlots(23, ΣCC 111). Числа детерминированы (проверено re-extract'ом). +15 тестов. Отложено (по дизайну): wily-стиль (метрики во времени) — поздняя надстройка надreview; comment-терм MI опущен (скоринг per-symbol). -
R1-C39 причина резолва на ребре + градуированная уверенность ✅ (S) — (2026-09-07, без схемы; решение:
docs/design/edge_resolution.md). Первое, что дал замер, — поправку к самой заявке: «у насresolutionговорит, разрешился ли вызов» неверно — наcallsтам уже стоит маршрут, шесть различимых значений. Перечисление пар(тип, resolution)по 13 сборкам показало три другие вещи: словарь открыт де-факто (значения печатают шесть модулей, никто их не перечисляет), поле перегружено (наreferencesтри значения из четырёх называют форму площадки, а не маршрут), и ничто не говорит, чего маршрут стоит —importedиregistry-candidateв артефакте равны. Сделано:RESOLUTIONSвmodel.pyрядом сEDGE_TYPES(сторож формы R1-C7, обе стороны), степеньexact | inferred | heuristic— порядковая, не вероятность (их0.9246621004453465наEXTENDS— вычисленное число, читаемое как точность, которой нет),callers/callees(min_confidence=…),confidenceвstats, степени вreport behavior. Схема не двинулась (D3): степень выводится, а не хранится — второй источник истины для чистой функции от первого однажды с ним разойдётся, и это уже случалось (schemaпротивtool_schema, R1-C25). Приёмка: пересборкаbquantпобайтно совпала по узлам и рёбрам (разошёлся толькоprovenance); 5533exact/ 49heuristic/ 3021 без маршрута;callers50 → 1 по 25 символам реестра, разница ровно 49. Каждая строка таблицы наблюдена — подreferences+importedпришлось строить отдельный зонд, ни одна фикстура её не давала. Сьют 887 → 901. Исходная формулировка (2026-09-01, из разбора OntoIndex): У нас на ребре естьresolution— разрешился ли вызов. У них на каждом ребре стоит как именно он разрешился и насколько этому верить:same-file0.95 (1082 ребра),import-resolved0.9 (2324),global0.5 (321) — последнее это сопоставление по имени, то самое, что мы считаем недопустимым выдавать без метки. Плюсmarkdown-link0.8 на импортах из доков иleiden-algorithmна членстве в сообществе. Scope: полеreason(закрытый словарь, как и остальной словарь рёбер) + числоваяconfidenceна call-рёбрах; экстрактор уже знает ветку, которая породила ребро, — сейчас она просто не доезжает до графа. Зачем: делает выразимым «дай только высокоточные вызовы» (confidence >= 0.9) и объясняет ребро, а не только утверждает его; это R1-C13 на уровень ниже — там метка на ответе, здесь на ребре. Приёмка: каждое call-ребро несётreasonиз закрытого перечня; счётчики по reason вstats; ни одно ребро не остаётся без причины (тест на полноту словаря); schema bump. Не берём с них: саму шкалу чисел — 0.9246621004453465 наEXTENDSпоказывает, что вероятность, полученная вычислением, читается как точность, которой нет. -
R1-C40 где именно правка достаёт до потока ✅ (S) — (2026-09-07, без схемы; решение:
docs/design/flow_reach.md). Первый пункт, взятый не по чужому issue, а по собственному разбору — после трёх релизов подряд из заявок соседей.Query.flows_to(): один обратный BFS от символа и его членов вместо N прямых обходов; секция вreport impact(markdown + json + MCP + serve-оп). Названоfirst_step, а неearliest_broken_step: граф знает, что символ на пути, и не знает, сломает ли его правка. Три неполноты названы порознь —non_call_refs(ссылка, которая в потоке появиться не может по построению),beyond_depth(достают дальше границы — посчитаны, не отброшены),in_call_graph: false(«сказать нечего» ≠ «ничего не достаёт», одиннадцатое применение «unknown≠none»). Приёмка: эквивалентность обратного обхода прямому — 1324 пары (символ, вход) на bquant и 252 на codemap, 0 расхождений;calculate_atr→ZoneAnalysisPipeline.runшаг 3;_type_checking_branches— 1 поток в пределах 5 шагов и 10 за пределом. Мутационно 2 покраснения. Сьют 870 → 887. Поправка к собственной приёмке: строка «ответ несётlimit-блок» заменена — серв-ответ полон, режет транспорт и объявляет это своим словарём (flows_shown/flows_total), как уже сделано для ссылок; сторож R1-C28 при этом расширен, чтобы имя видаflow_limitне проходило мимо классификации. Попутно:test_m1_query.py::test_cycle_detectionназывал конкретную пару модулей мишени, мишень 07.09 убрала эту аннотацию, и тест покраснел на чужом дереве при верном ответе инструмента — переписан на инвариант (три вида не пересекаются, каждый замкнут той областью, которую заявляет). Исходная формулировка (2026-09-01, из разбора OntoIndex): Ихimpactпо каждому задетому процессу отдаётaffected_process_count,total_hitsиearliest_broken_step— на каком шаге исполнения правка впервые достаёт до потока. У нас естьflows(прямой обход от входа) и естьimpact(кто ссылается), но они не сходятся в одном ответе: сказать «этот сценарий сломается, и первым — на третьем шаге» codemap сейчас не может. Scope: вimpact— секция по потокам, проходящим через задетые символы, с индексом первого задетого шага; переиспользует существующийflows-обход. Зачем: переводит blast-radius из «кто ссылается» в «что перестанет работать», а это и есть вопрос, который задают перед правкой. Приёмка: на bquant для символа из середины пайплайна названы потоки и номер первого задетого шага; число потоков сходится сflows; ответ несётlimit-блок (R1-C28). -
R1-C41 манифест скоупа описывал не тот набор файлов, из которого построен граф ✅ (S) — (2026-09-02, без схемы; заведено второй мишенью, issue #15, рядом с подтверждением починки R1-C36 как отдельный, ею не закрываемый пункт). Гэп:
gaps/scope_membership_2026-09-02.md, решение:docs/design/scope.md§1.7. Причина: на вопрос «что было входом» отвечают два независимых перечисления, и ничто их не сравнивает — манифест в git-режиме перечисляет отслеживаемый набор (git ls-files), экстрактор обходит файловую систему. В fs-режиме они совпадают по построению (замер: 4 = 4), в git-режиме это разные множества. Замерено на 0.0.7, без подставного каталога: неотслеживаемый (git addещё не делали) или игнорируемый модуль есть в графе, отсутствует вscope.filesи не двигаетscope_id— два графа с разным содержимым пришли с одной идентичностью входа (sha256:2b1dc442aadbe1f1e00…).module_count_diagnosticпри этом молчит правильно: счёт экстрактора и граф сходятся, манифеста в том сравнении нет. Это не отчётность:scope_id— ключ кэша--incrementalи пробаwatch; замер дал[incremental] unchanged: 0 module(s) recomputedнад файлом, только что отрастившим новый символ. Дизайн ограничение знал и делегировал егоinclude-оверлею (§1.5), но в git-режимеincludeприменяется послеls-files— он может убрать файл и никогда не может добавить; у продуктовогоresolve_scopeспеки нет вовсе. Scope: D1 — в git-режиме добавитьgit ls-files --others --exclude-standard(ровно тот набор, что встал бы отgit add .), записи сtracked: falseи безgit_blob; цена замерена — 4 мс против 2 мс. D2 — диагностикаSCOPE_MEMBERSHIP:fileкаждого узла обязан резолвиться вscope.files; игнорируемые файлы намеренно остаются вне манифеста — про них честный ответ сказать, а не усыновить. Только прямое направление: обратное шумит законно (23 из 187 на своей репо-сборке, все объяснены). D3 — результат сравнения едет вprovenance.inputsрядом соskipped, потому что stderr сборки до потребителя не доезжает. Ловушка: ровно эти файлы чаще всего несут абсолютный путь, а блок provenance path-free по контракту иbuild_provenanceна таком пути бросает — писать{count, sample, outside_root}со сведением пути к базовому имени, иначе диагностика превращается в падение сборки. Решено: схему не бампаем — поле аддитивное, формой какskipped. Приёмка выполнена: неотслеживаемый модуль попадает в манифест сtracked: false, двигаетscope_id(2b1dc442…→71e77b60…) и пересчитывается инкрементальной сборкой (былоunchanged: 0 module(s) recomputedнад файлом, только что отрастившим символ); игнорируемый назван и диагностикой, иprovenance.inputs; чистые сборки — 0 нарушений; идентичность на чистом дереве не сдвинулась (sha256:2c1da6e0…старым и новым кодом на одном архиве); бенчмарк R2 пере-материализован вsha256:300e0a01…5e47d2, 280 файлов,verify ✓. Мутационно: снятие--othersкрасит 3 теста, снятие проводки в provenance — 4. Сьют 734 → 743. Ловушка, которой дизайн не предвидел (найдена до написания проверки): у двух сторон разное начало координат — пути узлов относительны базе графа (R1-C31), пути манифеста — корню скоупа; на раскладкеsrc/наивное сравнение строк дало бы 2 ложных из 2 на здоровой сборке. Путь узла приводится к корню скоупа через известную на сборке базу, лексически, безresolve(). Поправка к собственному замеру: первая версия гэпа говорила «не бьёт ни разу» — померен был только класс--ignored. Репо-сборка догфуд-мишени в тот же час дала 262 файла манифеста, два из них неотслеживаемые (новые тестовые модули, ещё не добавленные в индекс): класс, предсказанный в §2 и не проверенный в §4, был активен ровно тогда. -
R1-C42 deep-тир не воспроизводим, и предел был назван не там, где его читают ✅ (S) — (2026-09-02, без схемы; заведено второй мишенью запиской в их ветке — «либо дефект, либо цена deep-резолвинга, и тогда назовите её в документации»). Гэп:
gaps/deep_tier_nondeterminism_2026-09-02.md, решение:docs/design/deep_tier_union.mdD6. Их замер: семь сборок неизменённого дерева, в одной пропало настоящее ребро черезgetattr(9524 против 9523), рядом плавал счётчик досьеexternal/unresolvedпри неизменныхout/resolved. Воспроизведено у себя: десять deep-сборок → два различных артефакта (7/3), дельта — 0 рёбер и 2 узла из 2133 (extras.callsу_resolve_nameи_resolve), тот же переходexternal ↔ unresolved. Локализовано до трёх вызововtgt.startswith; порядок обхода модулей идентичен во всех прогонах, экстрактор однопоточный. Отвергнуто по 10 прогонов на гипотезу:PYTHONHASHSEED=0(5/5 — гипотеза мишени опровергнута с мощностью), холодныйXDG_CACHE_HOME(5/5),InterpreterEnvironmentбез сабпроцесса jedi (6/4),gc.disable()(7/3),setarch -Rбез ASLR (5/5), свежийjedi.Projectна файл (3/7). Стабильна единственная конфигурация — изолированный запрос по одному файлу: 100/100. Механизм: пофункциональные счётчики исполнений jedi; исчерпавший бюджет вывод возвращает пустое множество, а пустое на нашей границе неотличимо от «нечего искать» и пишетсяunresolved. На первом расходящемся месте бюджет отличается на единицу при полностью совпадающих предыдущих 272 классификациях. Почему на единицу — не установлено, это внутри jedi. И вторая половина, наша: предел был известен (R1-C9, комментарий вci.yml), при этомdocs/ci.mdссылался наprovenance.md, где стояло обратное; README трижды говорилdeterministicбез тира; единственное место, где предел назывался, —docs/incremental.md, и там причина была неверна («cache-warmth-dependent», опровергнуто холодным кэшем). Потребитель прочитал написанное и объявил регрессию, которой не было. Сделано: нотаdeep_tier_unstableуровня note на каждомtier: deepграфе (не warning — граф не недействителен, он выборка);caveatsвcomparability()отдельно отdifferences(две deep-сборки остаются сравнимыми, шумовой порог печатается над вердиктом; на смешанных тирах caveat не выдаётся, чтобы мягкий сигнал не разбавлял жёсткий); числа в README,provenance.md,ci.md(с прямым признанием ложной ссылки),attribute-edges.md,incremental.md. Намеренно не сделано: не поднимаем лимиты jedi вслепую (причина разброса не установлена — это была бы догадка под видом починки); не усредняем N прогонов (всё равно выборка, цена N×); не переносим CI-гейт на deep (fast — подмножество deep по R1-C26, гейт ничего не теряет). -
[~] R1-C43 инкрементальная deep-сборка консервирует шум и слепнет от него 🟡 (S сделано, M открыто) — (2026-09-02, без схемы; заведено из собственного §9 предыдущего гэпа — пункт «чего не проверяли», продублированный потребителю как названная, но не замеренная гипотеза). Гэп:
gaps/incremental_noise_persistence_2026-09-02.md. Замерено (копияbquant@cf7c6e1, 88 модулей, зонд — реальноеaccesses-ребро, присутствующее в 6 из 8 полных сборок): (1) выборка замораживается — от графа без ребра пять инкрементальных сборок вернули его 0 раз, полные сборки того же дерева — 5 из 5; (2) промах защищает сам себя — правило инвалидации (b) читает старый граф, поэтому потерянное ребро отключает пересчёт, который его бы вернул (доказано контрастом: та же правкаzone_features, старый граф с ребром → писатель инвалидирован, без → нет; импортной связи между модулями нет, правило (a) не при делах); (3) оговорка «пока модуль снова не изменится» неверна — пересчитанный модуль даёт ребро лишь в 4 из 9. Сделано (дверь 1 — раскрытие):provenance.incremental, пишется всегда, включаяfalse(отсутствие = граф старше поля, то естьunknown, а не «полная»; R1-C28); нотаincremental_deep_spliceуровня note на deep-графах со splice, с прямым «не проверяй это повторной инкрементальной сборкой»; fast-тир исключён (там байт-идентичность полной сборке пиннится сьютом — замораживать нечего); планка R1-C9 разведена на два утверждения вместо одного: содержимое совпадает с полной сборкой, а артефакт не выдаёт себя за неё. +11 тестов. Отвергнуто по замеру: «не сплайсить модуль, признавшийunresolved > 0» — 69 из 88 модулей (78%), выше_FULL_FALLBACK_FRACTION, правило вырождается в полную пересборку. Сузить структурно нельзя: множествоunresolvedпо определению не имеет целей, его нечем проиндексировать по изменившемуся модулю. Открыто — две двери, обе меняют поведение и обе требуют своего замера: (2) периодическая полная —— отвергнута замером 04.09 на двадцати реальных коммитах bquant (watch/build --incrementalраз в N сборок делает полную, возвращая обратную связьgaps/incremental_chain_replay_2026-09-04.md): цепочка с базой--repeat 3без периодических пересборок — 6 рёбер-тиков промаха, с полной--repeat 3каждый пятый тик — 10, с одиночной базой — 16. Промахи приходят от одиночной выборки (база, fallback, пересчёт затронутых), не от возраста. Закрывается R1-C47. (3) ограничение вывода —всё, что заключает об отсутствии (dead-code, пустой— сужено замером 03.09, см. R1-C44 иimpact/callers,accessors), на графе соincremental: trueиtier: deepотвечаетunknown, а не «ничего»docs/design/absent_answers.mdD3/D4. Формулировка была написана до замера, и замер её не подтвердил: шесть полных deep-сборок замороженного дерева дают 1 символ из 2869 с гуляющим счётчиком ссылок, 0 достигают нуля, набор кандидатов dead-code одинаков во всех шести, уверенности не меняются; на тройке «истина / шум / заморожено» просели счётчики (impact7 → 4 ссылки), а вердикты — нет. Обесценивать всё, что говорит такой граф, из-за эффекта такого размера — перепуг там, где мы только что научились говорить точно. Вместо этого адресная квалификация операции (D4): вторая причина вepistemic, называющая сплайс, только у операций, читающих сплайсимые классы рёбер. Приёмка: список квалифицируемых операций выведен из списка сплайсимых классов, а не перечислен вручную, и это держит тест; на полном deep- и на fast-графе вторая причина не появляется. Замерено 03.09, и опровергнут наш собственный сигнал: «частичный пересчёт систематически слабее полного» — нет. Прогон 12×12 доведён и повторён вторым блоком, 24 пары на одинаковых состояниях дерева (codemap @10a75d5, jedi 0.20.0): состояние13/0— 5 из 24 в обоих плечах, среднееresolved11.83 против 11.96, знаковый критерий по 17 разошедшимся парамp = 1.000, зондовое ребро 15 против 18 (p = 0.581). Прежние «0 из 9» — артефакт отбора: пересчёт запускался правилом (b), которое читает старый граф, поэтому наблюдения приходили только оттуда, где ребро ещё было; триггер сменён на правило (a), не зависящее от состояния графа. Гипотеза «холодный проектный кэш» поддержки не получила. Вывод R1-C43 не ослаблен, а сужен до адреса: вред сплайса в том, что модуль не пересчитывается, а не в том, что он пересчитывается хуже. Побочно: у шума тира есть компонента уровня прогона — блоки различаются (13/04 против 1), и оба плеча двигаются вместе, что и оправдывает парную схему. Осталось незамеренным только более узкое: 24 пары хоронят крупный эффект, малый — нет. Приёмка: мутационно отключение ноты красит 3 теста (остальные три мутации — 16, 2, 8); 13 тестов, сьют 752 → 765. -
R1-C44 пустой ответ не говорит, какого он рода: «ссылок нет» и «символа нет» неразличимы ✅ (S) (2026-09-03, без схемы; найдено из собственной ошибки по ходу замера под дверь (3) R1-C43 — зонд искал
bquant.…на графе, корнящемся именем каталога, и восемнадцать глубоких сборок не измерили ничего, выглядя при этом как результат). Гэп:gaps/absent_symbol_answers_2026-09-03.md, дизайн:docs/design/absent_answers.md. Замерено:impactо символе, которого в графе нет вовсе, отвечает{"ok": true, "refs": [], "risk": "none"}— утвердительное «ничто от этого не зависит» о том, о чём инструмент никогда не слышал;callers/callees/flows— пустые списки; на реальном графеbquant(4840 узлов) конверт ответа про призрак и про существующий символ без ссылок совпадает побайтно, включаяepistemic. Диагноз точный и в одном месте:Query.canonical_info()возвращаетNone, то есть ответ «такого символа нет» вычислен, аhandle()выбрасывает его проверкой на ложность —if r and (r["ambiguous"] or …); плюс_canonотдаёт неразрешённый ввод дальше как id. Тяжелее двери (3), из-за которой нашлось: условия нет никакого — срабатывает на любом графе, включая полную сборку, и вердикт выдаётся всегда (у двери (3) замерено 0 перевёрнутых вердиктов). Решения: D1ok: true+resolved.found: false, вердикт →unknown(отвергнутоok: false— сломает тех, кто законно прощупывает id пачками:reviewпо диффу,checkпо контракту); D2 тернарная развилка вместо проверки на ложность. Попутно замер вскрыл, чтоreferences_toиaccessorsчитают сплайсимые классы и не входят в_PARTIAL_OPSвовсе — не несут даже общегоepistemic; чинится независимо от инкремента. Приёмка: ответ про призрачный id и про реальный символ без ссылок различимы программно на каждой операции, где сегодня совпадают;Query.impactна неизвестный id даётrisk: "unknown"; ни одна операция не начинает возвращатьok: falseтам, где возвращалаok: true; мутационно возвратif rобязан покраснить тест, и тест подаёт именно несуществующий id, а не только неоднозначный (R1-C37). Урок метода, записанный в гэпе §6: к правилу «проверка, которой не показывали то, что она обязана отвергнуть, — не проверка» добавляется симметричная половина — зонд, которому не показывали то, что он обязан найти, — не зонд. Одна сборка с печатью нескольких id стоила бы 70 секунд; вместо этого восемь отрицательных ответов были прочитаны как измерение. Сделано 04.09 (D1–D6 как рекомендовано):_canonпишет резолюцию всегда — найденную сfound: true, ненайденную как{input, id: null, found: false};handle— трёхходовая развилка вместоif r and …;_op_impactрезолвит сам (черезimpact_targets, не_canon) и потому записывает сам;_op_query— тоже, и попутно закрыт F23-пробел наquery:findсопоставляет только короткое имя, полный id изsearchоткрывал пустое досье — теперь резолвится черезcanonical_info.Query.impactна неизвестный id →risk: unknown+risk_reason; рендер impact говорит «nothing is known about it». D4: таблица_OP_EDGE_CLASSES, из которой выводятся_PARTIAL_OPS(+accessors) и_SPLICE_OPS;SPLICED_EDGE_TYPES— вmodel.py, потому чтоserveиextractне имеют права импортировать друг друга (контрактindependentпоймал первый вариант — 4 красных в R1-C37); вторая причина — ключepistemic.spliceприtier: deep+incremental: true, D5 — отдельная формулировка при отсутствии поля. D6: справка--incrementalтир-квалифицирована;--incrementalс--consumer/--docsпечатает[note] … was not applied; справкаpathи README говорят, что id корнятся именем каталога. Приёмка выполнена: 34 теста, каждый «не найден» подан настоящим призрачным id; призрак и реальный символ без ссылок различимы на всех восьми резолвящих операциях;Query.impactпризрака —unknown, одинокого —none; ни одна операция не стала возвращатьok: false; каждый сплайсимый класс заявлен хотя бы одной операцией (добавили класс — тест требует решения); на полном deep и на fastspliceне появляется; сьют 800 → 834. -
R1-C49 у цикла три вида, по самой слабой области, которая его замыкает ✅ (S) (2026-09-07, без схемы). Источник: codemap#18 — лаборатория, единственный потребитель с обоими правилами: R1-C48 перенёс их
TYPE_CHECKING-цикл изno_cyclesвno_lazy_cycles, одно красное сменилось другим. Гэп:gaps/type_only_cycles_2026-09-07.md, дизайн:docs/design/type_only_cycles.md. eager / lazy / type-only,no_lazy_cyclesсужается до функционально-локальных, новоеno_type_only_cycles(по умолчанию выключено), все потребители печатают три числа всегда. Приёмка снята: bquant6b17e35— 0 / 9 / 1 (на 0.0.13 было 0 / 10), codemap — 0 / 0 / 0 при одном импорте подTYPE_CHECKING, игрушка с одним type-only циклом зелена на обоих старых правилах и красна на новом; смешанная пара остаётся lazy (ранг областей в экстракторе:module<function<type_checking). -
R1-C48 импорт под
if TYPE_CHECKING:— третья область видимости, не eager-ребро ✅ (S) (2026-09-06, без схемы). Источник: codemap#18 — сессия bquant поставилаno_cycles = trueи получила красный гейт на циклеzones.cache ↔ zones.pipeline, где единственный импорт стоит подif TYPE_CHECKING:и не выполняется никогда. Гэп:gaps/type_checking_imports_2026-09-06.md, дизайн:docs/design/type_checking_imports.md. Воспроизведено (bquant6b17e35, fast): ребро с пустымиextras, 271 module-level / 26 function-local, 9 ленивых циклов. Приёмка: гейт зелёный; побайтово ровно одно ребро получаетscope: type_checking;import_mapназывает его всегда; дерево codemap (свой такой импорт вmcp_server) — контракт зелёный, ленивые циклы без изменений; игрушка на все формы условия с мутацией. Приёмка снята (7b2f602, то же дерево): гейт зелёный, «10 не судилось, 1 подTYPE_CHECKING»,import_map270 / 26 / 1; побайтово 2965 узлов те же, из 8617 рёбер отличается ровно одно; codemap сам — 104 / 41 / 1, контракт зелёный. Формулировки пяти потребителей («closed only by a function-local import») переписаны на «non-eager import». -
R1-C47 инкрементальная цепочка сэмплирует N раз везде, где сэмплирует вообще ✅ (M) (2026-09-04, без схемы; закрывает дверь (2) R1-C43 — другой починкой, чем была открыта). Гэп:
gaps/incremental_chain_replay_2026-09-04.md, дизайн:docs/design/incremental_resample.md. Замерено (codemap 0.0.11 @aee9889, реальная историяbquant— базаf65fca1и 20 следующих коммитов поbquant/, каждый тик против свежей--repeat 3того же коммита): одиночная база + инкремент — 16 рёбер-тиков промаха, ребро, потерянное базой на тике 0, отсутствует и на тике 20; база--repeat 3+ инкремент — 6; то же + полная--repeat 3каждый пятый тик — 10. Каждый промах восходит к одиночной выборке: базе, fallback-сборке (коммит на 63 модуля из 88 сбросил все цепочки в одиночную полную на 3-м же тике) или пересчёту затронутых модулей на тике. Возраст графа — не переменная: потерянное ребро не возвращается со временем, оно ждёт пересчёта модуля, а пересчёт — та же монета. Решения: D1 —--repeat Nдопускается с--incremental; N — свойство цепочки, записано вprovenance.samples.runs, другой N = другой строитель (mode: full,reason: samples-changed) — пересматривает D8deep_tier_union.md, чьё основание (seenотносительно разных N) верно только при смене N; D2 — все три места выборки берут N (база уже с 0.0.11; fallback →collect_samples; пересчёт затронутых → N воркеров сonly=affected, структурная база строится в каждом),unstableпересчитывается по итоговому графу; D3 — стоимость тика по стене ≈ сегодняшняя при N свободных ядрах, память × N; D4 — периодическая полная не делается; D5 —watch --repeat N, флаг в рецепте сайдкара. Приёмка: на игрушке deep — базаrepeat=2, правка,update_graph(repeat=2)→incremental,samples.runs == 2; запросrepeat=3над ним →full,reason: samples-changed; правка всех модулей → fallback сruns == 2;seenна ребре незатронутого модуля переживает сплайс и учтён вunstable; CLI--deep --incremental --repeat 2— exit 0 и второй запуск инкрементальный, fast +--repeat— по-прежнему 2;_watch_build_argvнесёт--repeat; R1-C9 не тронут (путьrepeat == 1— прежний код); на реплее — четвёртое плечо (база--repeat 3+--incremental --repeat 3на каждом тике) против сохранённой истины даёт не больше 6 рёбер-тиков, и fallback-тик больше не одиночная выборка. Сделано 04.09._sample_worker/collect_samplesполучилиonly=(тот же хук, что R1-C9);update_graph(repeat=N):_same_builderвозвращает причину и считаетsamples.runsчастью строителя (samples-changed); fallback →extract(repeat=N); пересчёт → N воркеров по затронутым, слияние, сплайс,unstableпересчитан по итогу. CLI: сочетание разрешено, fast +--repeatпо-прежнему exit 2;watch --repeat N, рецепт сайдкара несёт флаг. Приёмка выполнена: игрушка — все пункты; реплей, плечо D (--repeat 3база +--incremental --repeat 3на каждом тике) — 3 рёбер-тика промаха против 6 / 10 / 16, на тике 20 — 0, fallback-тик — полная--repeat 3(88 с), обычный тик 11–28 с; 8 тестов, сьют 834 → 842. -
R1-C45 одна deep-сборка — выборка с известной долей; совет «пересобери» обязан называть N ✅ (M) (2026-09-04, без схемы; заведено второй мишенью — issue #16: 175 полных deep-сборок дерева
bquant, тринадцать плеч, живое ребро восстанавливается в 75 % (126/168, χ² df=12 p=0.19), пять факторов исключены). Гэп:gaps/deep_tier_union_by_repeat_2026-09-04.md, дизайн:docs/design/deep_tier_union.mdD7–D9. Пересматривает нашу же строку D6 (R1-C42): «не усредняем N прогонов — всё равно выборка, цена N×». Первая половина верна; вторая — цена, а не довод, и её надо было поставить рядом с тем, что она покупает: одна сборка не видит измеренное ребро в 1 случае из 4, три — в 1 из 64. Замерено у себя (bquant@cfc97c2, 242 файла, 8 полных сборок по 95–106 с,codemap 0.0.10 @ea517b9): 4850 узлов совпадают во всех восьми; рёбер 13675 в объединении, 13674 во всех восьми, нестабильно 1 — то жеaccesses-ребро, что зонд мишени, 5 из 8; вне jedi-классов нестабильных 0; несравнимых пар 0 из 28; объединение любых трёх сборок равно объединению восьми в 55 из 56. Счётчик узла шумит сильнее ребра (три вариантаattr_accessза одним ребром — сайты дедуплицируются в цели). Ключ объединения — не(type, source, target), как предлагала мишень: внутри одной сборки 96 таких троек несут по две записи (чтение и запись одного поля; аннотация и значение;constructиdeepс разных сайтов), все 96 стабильны. Идентичность — по классу:accessesразрешается формой-сначала, jedi только на локальном приёмнике, метка — свойство сайта, шум только присутствие (4649 ключей × 8 — ни одной смены метки) → полный ключ;calls— jedi-сначала с фолбэком (R1-C26 D2), тот же сайт выходитdeepилиimported→ ключ(source, target, via)(уникален внутри сборки, 2807 рёбер), вариантdeepпобеждает целиком. Решения: D7build --repeat N— структурный слой один раз, поведенческий N раз, слияние по классу; ребро, виденное в k < N прогонах, несётextras.seen: k; счётчики узла — вариант с minunresolved;provenance.samples = {runs}всегда (и на fast),unstable— только при N ≥ 2 (при N = 1 не измеримо: отсутствие = не измерено, R1-C28);--repeat > 1на fast — exit 2 с причиной, не молча. D8 — только полные сборки; с--incrementalвзаимоисключимы (сплайс — чужая выборка по определению,seenстало бы относительно разных N);refreshнаследует из сайдкара;watchне получает. D9 — нотаdeep_tier_unstableв двух формулировках поsamples.runs, обе с числами: при 1 — измеренная доля и что даёт N; при N ≥ 2 — сколько рёбер несутseenи остаточный промах 0.25^N «при измеренной доле»; caveatcomparability()называет число выборок с каждой стороны. Приёмка: синтетическая пара выборок (importedпротивdeep+ метод) сливается вdeepи метод сseen: 1, пара read/write на одном ключе выживает как два ребра;--repeat 3на замороженномbquantдаёт множество рёбер, равное объединению трёх выборок,samples.unstable= числу рёбер сseen, все не-jedi классы совпадают побайтно;--repeat 1не меняет ни байта рёбер на обоих тирах (толькоprovenance.samples); fast +--repeat 2и--incremental+--repeat— exit 2 с сообщением; мутационно выключение веткиruns ≥ 2красит тест, которому подан настоящий union-граф (R1-C37). Чего не мерили: формуimported ↔ deep(в восьми сборках не встретилась — правило дляcallsпроверяется синтетикой); долю на других рёбрах/деревьях; рёбра с долей 0.95+ (восемь сборок их не отличают от стабильных); аномалию мишени 1 из 7. Сделано 04.09, с одним поворотом по замеру. Первый черновик повторял поведенческий слой N раз в одном процессе на копиях структурной базы; замер до кода (8 проходов в одном интерпретаторе): два артефакта сериями 1 | 2–6 | 7–8 — независимость проходов внутри процесса не установлена, а доля 75 % измерена между процессами. Поэтомуcollect_samplesподнимает свежий интерпретатор на сэмпл (spawn, неfork) и гонит их параллельно; живёт вgriffe_extractor.pyрядом с тем, что зовёт воркер — первый вариант вunion.pyтянулся назад функционально-локальным импортом, и наш же контрактno_lazy_cyclesего поймал (2 красных в сьюте).union.py— чистое слияние;_BEHAVIOR_CALL_RESинкремента теперь импортируется оттуда (один список). Приёмка выполнена (деревоbquant@cfc97c2, инструмент = этот коммит, однопоточные сборки):--repeat 3 --mode fullсо всеми корнями — 109 с стены против ~100 с одной сборки, 4850 узлов / 13675 рёбер = ровно объединение восьми последовательных сборок, одно ребро сseen(то самое,seen: 1),samples.unstable == 1 == len(seen), всеseenв jedi-классах, не-jedi классы и узлы побайтно = сборке g1, дублей 0;--repeat 8по ядру — 119 с,seen: 5из 8 — та же доля, что у восьми отдельных процессов (5/8);--repeat 1— рёбра bquant (2868/8407) и codemap (1045/3029) на fast побайтно = старому инструменту, новое толькоprovenance.samples; fast +--repeat 2и--incremental+--repeat— exit 2; нота в двух формулировках, вторая проверена на настоящем union-графе; 23 теста; сьют 765 → 800. -
R1-C46 имя, определённое дважды в одной области, — находка, а не дубль ребра ✅ (S) (2026-09-04, без схемы — один новый открытый ключ
extras; заведено второй мишенью — issue #16 §5: записьcontainsдважды по полному ключу, за ней метод, определённый в классе дважды; у себя они починили в тот же день). Гэп: тот же, §5; дизайн:docs/design/shadowed_definitions.md. Воспроизведено на игрушечном пакете, и это хуже дубля: griffe оставляет последнее определение без следа первого; обход consumer-корней даёт узел и реброcontainsна каждыйdef(дубль мишени); поведенческий проход обходит оба тела и приписывает оба одному узлу —Thing.getполучаетcalls → oneиcalls → two, тогда какone()не исполняется никогда. Фантомное ребро — класс R1-C13 с другой стороны: не ребро в никуда, а ребро из ниоткуда;impact(one)отвечает «есть вызывающий», dead-code молчит. Решения: D1 — в обходчиках (_named_functions,_named_functions_scoped,_defs) живое тело = последнее безусловное определение id; ранние безусловные — не обходятся, их строки пишутся вextras.shadows: [lineno, …]на выжившем узле (функции и классы); условные (if/try/with/match) — ни затирают, ни затираются, обходятся как сегодня. D2 — три идиомы исключены по декоратору, не эвристикой:@overloadна раннем (единственное повторное определение в дереве codemap — фикстураhardpkg/modern.py, и это оно);.setter/.getter/.deleterна позднем;.registerили имя_. D3 —Query.shadowed_definitions()и отдельная секция «certain» вreport dead-code, не graded-кандидат: тело, которое не может исполниться, — не «probably». Отвергнуто: диагностика уровня графа (факт про символ, не про граф); только дедупcontains(симптом мишени, а фантом остаётся). Приёмка: на игрушечном пакете уThing.getровно одно реброcalls(→two),shadows == [8],calls.out == 1; как consumer-корень в--mode full— одноcontains;report dead-codeпоказывает секцию;@overload, setter,_/registerиif TYPE_CHECKING— безshadows, оба тела в графе; мутационно возврат недедуплицированного обхода красит тест на фантомное ребро, и тест подаёт настоящий дубль (R1-C37); графы bquant и codemap на fast-тире побайтно совпадают до и после (ни в одном дереве сегодня нет затенённого определения). Сделано 04.09:_shadow_mapвbehavior.py— одна двухфазная карта (пропуск поддерева + записи), которую читают все четыре обходчика (_named_functions,_named_functions_scoped,_defs,_func_ranges) и обход использований consumer-корней (сайт внутри мёртвого тела — не использование);extras.shadowsставится в поведенческом проходе и в_materialize_defs;shadowsдобавлен в сплайсимые экстры инкремента, иначе R1-C9 (байт-идентичность на fast) ломался бы на нём;Query.shadowed_definitions(), секция «Shadowed definitions — certain» вreport dead-codeи полеshadowedв его JSON. Приёмка выполнена: фикстураshadowpkg— одно реброcalls(→two),shadows == [21],out == 1; затенённый класс —shadowsна узле класса, методы раннего тела не обходятся; consumer-корень — одноcontains, использования только из живого тела; четыре идиомы — безshadows, оба тела в графе; fast-графы pinnedbquant@cfc97c2(2868/8407) и дерева codemap (1045/3029) побайтно совпадают по узлам и рёбрам со сборкой инструментом до правки; 12 тестов.
-
R1-C5 Двух-графовый diff + API breaking-change ✅ (2026-08-14, без схемы).
codemap/apidiff.py(движок: added/removed/changed по публичной поверхности; сигнатуры парсятся черезast—def <sig>: ...— точный разбор параметров, не строковый diff; непарсящаяся → консервативныйsignature-changed, не ложный breaking). Правила breaking: удалённый публичный символ · public→private · смена kind · удалён параметр · добавлен обязательный · optional→required · удалён*args/**kwargs. warning: смена типа параметра/возврата · newly-deprecated. info: добавлен optional-параметр · новый символ.codemap diff old new [--exit-code](гейт релиза: exit 1 на breaking) + serve-opdiff+ MCP-tooldiff+ влито вreview --base(добавляет removed/added/breaking, которых не видят хунки). Приёмка выполнена: на паре графов до/после diff помечает breaking (param made-required, removed symbol), added/deleted перечислены; тест на непарсящейся сигнатуре. Докиdocs/api-diff.md. +21 тест. -
R1-C6 Relevance-ранжирование + token-budgeted pack ✅ (2026-08-22, без схемы). codemap стал first-class context-provider. (a)
Query.rank(seeds, root)— personalized PageRank по usage-рёбрам (calls/imports/references/inherits/implements, user→used); без seeds — глобальная важность (хабы наверх), с seeds (id/короткое имя/путь файла) — relevance к контексту (приём aider). PageRank — чистый Python power-iteration (без numpy/scipy — codemap лёгкий, как CC/MI без radon), детерминирован (sorted обход, округление). (b)codemap pack --budget N [--seed X](serve/pack.py) — ранжированный срез под токен-бюджет: жадно добавляет по убыванию ранга, пока влезает; оценка токенов ≈ 4 симв/токен (детерминированно, без токенайзера). Поверхности: CLIpack, serve-oppack, MCP-toolpack. Приёмка выполнена: ранкинг детерминирован ✅;packникогда не превышает бюджет ✅ (проверено на 5/30/100/100k); на bquant топ-хабы (logging_config/config/exceptions) раньше листьев ✅; seed=analyze_zones→ наверх zone-символы вместо глобальных хабов ✅. +12 тестов. Докиdocs/pack.md. Закрывает последний Tier-2-пункт. Отложено: bit-точный токенайзер (эвристики хватает). -
R1-C7 Закрытый словарь edge-kind + структурные descriptor-id ✅ (2026-08-22, без схемы). Структурные descriptor-id доказаны SCIP-экспортом (R1-C1). Остаток закрыт:
model.EDGE_TYPES— закрытый frozenset из 10 типов (contains/imports/export/inherits/decorated_by/calls/references/ implements/reads/writes) с doc по каждому; узлы остаются открытым множеством (по DESIGN §2, edges — typed). Тестtests/test_r1c7_edge_vocab.py: (1) пинует набор литералом; (2) падает, если живой граф эмитит незадекларированный тип; (3) падает, если задекларированный тип перестал появляться (нет мёртвого словаря). Побочно поймал дрейф: иmodel.py, и DESIGN §2 писалиexports, а код эмититexport— синхронизировано (ровно тот дрейф, который тест теперь стережёт). Доки:model.py+ DESIGN §2. +3 теста. -
R1-C8 Dead-code confidence + whitelist UX ✅ (2026-08-22, без схемы).
Query.dead_code(whitelist, min_confidence)— градуированные кандидаты (private-функция без входящего resolved-вызова) с провенанс-причиной: high (нет ни одного входящего ребра/хука), medium (декоратор/registry могут вызвать неявно — framework-hook/dispatch), low (на неё естьreferences— вероятно живая; причина называет, кто ссылается, по роутам). Low-тир = отсечка FP, которую call-only vulture не может. Whitelist (точный id /fnmatch-glob) из[dead_code].whitelistв codemap.toml +--min-confidenceфильтр.render_dead_codeгруппирует по уверенности с причинами;dead_symbols()остался тонкой обёрткой (обратная совместимость). Поверхности: CLIreport dead-code --min-confidence, serve report-op (min_confidence + whitelist из source_root). Приёмка выполнена: confidence ✅, whitelist уважается ✅ (dogfood: 42→4 после подавления argparse/dict-разводки_cmd_*/_op_*— ровно те framework-FP), провенанс-строка объясняет «почему не мёртвое» ✅. +10 тестов (фикстур-граф со всеми тирами + exclusions + whitelist/filter + loader). Докиdocs/dead-code.md. Закрывает предпоследний Tier-2-пункт. -
R1-C20 Рёбра доступа к атрибутам (
impact/refs для полей класса) ✅ (2026-08-23, схема 0.11) — из dogfood на bquant при планировании rename поля dataclass (G16). Гэп:impactна атрибуте класса возвращалrefs:[]иrisk:"none"при реальных сайтах — атрибут-ноды извлекались, но ни одно ребро в них не входило (reads/writesвсе наcolumn:*, M12). Аффирмативная ложь «ничего не зависит» там, где нуженunknown. Сделано: новый закрытый edge-typeaccesses(extras.access ∈ {read,write},extras.resolution, R1-C7 + бамп схемы 0.10→0.11) + пассextract/attrflow.py(по образцуdataflow.py):self.field(self),ClassName.field(class), construction-kwargs → write (construct) — fast-tierast;obj.fieldна типизированном локале (deep) — deep-tierjediчерез инференс типа получателя (не goto — goto садится на assignment внутри метода, а не на ноду поля); неразрешённое = счётчик (extras.attr_access), не ребро в неузел (R1-C13-f2).accessesвплетён в_IMPACT_EDGES(атрибут-скоуп — колонки остаются вне impact);Query.readers/writers, serve-opaccessors+ MCP-tool, блокattributesв досье, CLI-рендер. P0 honesty:impactна атрибуте без смоделированного accessor'а →risk:"unknown"+risk_reason, никогдаnone(для функции/класса пустой radius остаётся честнымnone). Приёмка выполнена: на bquant fast-tier 1619accesses-рёбер (self 1027 / construct 587 / class 5), deep +158;impactнаSwingThresholds.zigzag_deviationдаёт 6 refs (было[]/none); 210 атрибутов без accessor'а честноunknown✅. +15 тестов (фикстураattrpkg: все формы + property-нода griffe + soundness-гейты + deep). R1-C7 guard-тест обновлён (accessesв словаре + на dogfood-графе). Открытие в ходе: griffe моделит@propertyкак attribute-ноду, не function → чтениеobj.prop— легитимноеaccesses-ребро (D-corr, §7 дизайна). Доки:docs/attribute-edges.md, дизайнdocs/design/attribute_edges.md(D1–D4 закрыты: B/да/да/да), гэпgaps/attribute_impact_gap_2026-08-22.md. Смыкается с M12, R1-C7, R1-C13. Issue #1. -
R1-C21 Плоский layout: краш на namespace-каталоге + молчаливые 0
imports-рёбер ✅ (2026-08-24, без схемы) — из попытки применить codemap на втором реальном таргете (приватная research-лаба рядом с bquant, движок в плоском каталогеshared/); оба дефекта пойманы в первые 20 минут на первой же сборке — сигнал по оси B2 (робастность), которого один dogfood-таргет дать не мог. Гэп (две половины одного layout'а): (a) без__init__.pygriffe считает каталог namespace-пакетом, у которогоfilepath— этоlist[Path]→TypeError: ... not 'list', причём сообщение не называет ни каталог, ни причину; (b) с__init__.pyсборка успешна и даёт 0imports-рёбер: пакет грузится от родителя (griffe_extractor.py:47-48), id получаютсяflatpkg.beta, а griffe пишет таргет как в исходнике (alpha.X) →_resolve_edges(:164-166) отбрасывает его как внешний, неотличимо отpandas.DataFrame. Почему (b) хуже краха: отсутствие данных рендерится как справка о здоровье — «no layer violations», «граф импортов ацикличен», аdead-codeобъявляет все модули orphan (на реальном таргете — все 35 модулей работающего движка;impactна живом символе → «isolated», при grep 10+ файлов). Это тот же класс, что F14 и issue #3: уверенно выглядящее ничто. Замерено на месте (не со слов): игрушечный репро из 2 файлов даётCounter({'contains': 4}), оба модуля orphan; правило «переписать неразрешённый импорт, чья голова называет sibling-модуль» даёт 0 ложных срабатываний на codemap (45 модулей / 114 внешних импортов) и bquant (88 / 580); краш живёт в 5 местах-потребителяхfilepath(не в одном — после патча только_relсборка падает дальше вbehavior.py:61, guardif not fpнепустой список проходит). План (по убыванию ценности на строку): (1) честность — 0importsпри ≥2 модулях объявляется на билде, вstatsи в обоих отчётах до выводов (верно даже если резолв не чинить); (2) не падать — нормализацияfilepathв одной точке (Noneдля namespace) + внятное сообщение, что за каталог и чем это грозит; (3) резолв sibling-импортов с пометкой допущения на ребре (extras.resolution="flat"— это инференс проsys.path, а приближения у нас метятся, не прячутся); (4) CI-guard на плоскую фикстуру. Приёмка: графы bquant и codemap до/после — байт-в-байт идентичны (замер §1 дизайна предсказывает ноль переписываний, тест это фиксирует); плоская фикстура в обеих формах собирается, реброbeta→alphaесть и помечено. Доки: гэпgaps/flat_layout_gap_2026-08-24.md, дизайнdocs/design/flat_layout.md(D1–D5 открыты, рекомендации: A / A / да / нормализация+продолжать / derive). Разблокирует класс таргетов: переделка той лабы в настоящий пакет = переписать импорты в 88 файлах, 52 из них — замороженные эксперименты, обязанные остаться байт-стабильными. Сделано:extract/gsource.py— точка нормализации:module_file()(Noneна namespace-списке, используется всеми 5 потребителями),is_namespace_dir(),module_imports()(квалифицирует плоские sibling-таргеты один раз — для всех пассов);griffe_extractor— двухпроходный резолв (сначала точный, потом_flat_sibling-ретрай) с пометкойextras.resolution="flat";codemap/diagnostics.py— дериваты, ничего не хранится:import_graph_diagnostic(0 imports при ≥2 модулях) иnamespace_target_diagnostic(выводится изfile is Noneу корня), подключены вcli build(stderr),stats.diagnosticsи три отчёта (architecture / dependencies / dead-code) — перед выводами, чтобы пустота не читалась как чистота. Промах дизайна, найденный первым же тестом: слеп был не только слойimports— слой вызовов тоже (behavior._resolveклассифицирует таргет по префиксу пакета, а у плоского его нет); починка на карте импортов, а не внутри_resolve, держит инференс в одном месте и заодно вылечилаattrflow. Приёмка выполнена: граф bquant до/после — байт-в-байт идентичен (fast),attrpkg --deep— тоже; инференс на здоровых пакетах срабатывает ноль раз (зафиксировано тестом на bquant); плоская фикстура собирается в обеих формах, реброbeta→alphaпомечено, package-qualified остаётся без метки; честная половина работает автономно. +20 тестов (tests/test_r1c21_flat_layout.py, фикстураflatpkg), сьют 369→389. Доки:docs/flat-layout.md, дизайнdocs/design/flat_layout.md(D1–D5 закрыты: A / A / да / нормализация+продолжать / derive), гэпgaps/flat_layout_gap_2026-08-24.md. Смыкается с R1-C13 (метки приближений), M18/#3 (поверхность сообщает, когда может врать). Issues #4, #5. -
R1-C21-f1 (flat layout) consumer-корни: голый импорт в ядро не давал ребра ✅ (2026-08-24, без схемы) — из повторного прогона R1-C21 автором на том же реальном репо. Что уже работало: внутри ядра
imports0→75 (всеflat),calls316→469. Гэп: consumer-корень (--consumer tests), пишущий тот же голый импорт, не давал ни одного ребра —roots.py::_consumer_importsотсеивает statement, если модуль не квалифицирован ядром, ещё до попытки резолва; аgsource.module_importsсюда не достаёт (он правит карту импортов griffe-модулей ядра, а consumer-корни сканирует отдельный ast-пасс). Почему это половина той же цены:--consumerсуществует ради вопроса «кто пользуется X по всему репо», и на плоском layout ответ оставался уверенным нулём («No inbound references — isolated»), при 8 файлах с реальными call-site вне модуля. Диагностика R1-C21 при этом молчала — импортов не 0, их 75, просто ни один не пересекает границу корня: проверка была написана на одно измерение уже, чем нужно. Сделано (D6):_CoreIndex.qualify_flat()+core_is_flat— кросс-рутовый инференс включается, только если само ядро плоское (namespace-корень либо в графе уже естьflat-рёбра). Гейт структурный, а не статистический: у нормального пакета каталог никогда не наsys.path, значит голыйimport configв скрипте не может вести вpkg/config.py, и ребро было бы выдумано (замер как вторичный аргумент: 0 ложных на 559 не-bquant импортов в его consumer-корнях). Гейт смотрит на улику, а не на наличие__init__.py: ядро с__init__.py, но с голыми импортами внутри — плоское по факту. Сделано (D7):cross_root_diagnostic— «≥1 не-core корень подан, но ни одна ссылка не доходит до ядра»; отдельная проверка на отдельное измерение, а не обобщение старой. Приёмка: repo-scoped граф bquant (core+tests+examples+docs, mode=full, 5.25 МБ) — байт-в-байт идентичен; на упакованном ядре голый импорт остаётся неразрешённым, квалифицированный работает без метки. +9 тестов, сьют 389→398. Поправка автора к #5: «grep находит в 10+ файлах» считало упоминания, не call-site; честно — 13 упоминаний, 9 файлов с реальными вызовами (8 вне своего модуля). Гэп верен в любом случае, но цифра была измерена небрежнее, чем звучала. Issue #6. -
R1-C21-f2 (diagnostics) заметка выдавалась за предупреждение ✅ (2026-08-24, без схемы) — из issue #8. Гэп: рендереры отчётов дописывали одну зашитую подпись ко всякой диагностике — «findings below are derived from that empty import graph — unknown, not clean». Для проверки на пустой граф верно; для namespace-заметки — нет, и на репо с 404 import-рёбрами корректный раздел («Orphan modules: 0») подписывался как недостоверный. Это зеркало того самого дефекта, ради которого баннеры и заводились: отсутствие не должно читаться как здоровье, но и верный ответ не должен читаться как отсутствие. Сделано: severity и consequence принадлежат проверке, а не презентеру —
warningобесценивает выводы ниже и сам приносит фразу об этом,noteсообщает факт и не приносит ничего; единый рендерdiagnostics.render_lines(⚠️ /ℹ️), CLI печатает[warning]/[note]. Ни одна поверхность больше не может приписать смысл одной проверки условию другой. +3 теста, сьют 398→401. -
R1-C22
high-полоса dead-code врёт в 39% (ссылки, которые видны в исходнике, но не в графе) 🆕 ✅ (2026-08-24, без схемы) — из issue #7, найдено при проверке flat-фиксов: «рабочий граф и сделал это видимым». Не про плоский layout — воспроизводится на самом codemap. Гэп:high(«no inbound calls, references, or decorators») выставляется функции, на которую есть ссылка в её же модуле, просто в форме, которой граф не моделирует.high— это полоса, по которой действуют (и ровно её оставляет--min-confidence high). Замерено — механизма три, а не один (каждый пакет показывает только два, поэтому один dogfood-таргет этого дать не мог): ① функция-как-значение (значение dict, аргументdefault=) — codemap 13, bquant 0; ② вызов на уровне модуля (_register_all_indicators()при импорте) —behavior.pyобходит только именованные функции, модульные statements не посещаются вовсе: codemap 0, bquant 2; ③ вызов внутри вложенного def — весь вложенный def отбрасывается (nested closure — not a definition node), вместе с его вызовами: codemap 4, bquant 1. Итого 20 из 51 (39%) ложных. ② и ③ — это отсутствующиеcalls-рёбра, то есть страдает не только dead-code:impact,callers,flowsи архитектурные виды читают граф без импорт-тайма и без тел замыканий. План: грейдер не трогаем (_grade_deadуже демотит вlowпо любому входящему ребру — фиксы проходят сами): ①references-ребро сextras.resolution="name"(~69 рёбер на codemap, ~149 на bquant; тип не новый —referencesв словаре и так «dispatch site → символ, который он называет»); ②callsот узла модуля (27/137 сайтов); ③ атрибуция вызова ближайшему объемлющему узлу графа — дорогая (619 / 5202 сайта), поэтому дельту меряем до мержа и при непропорциональности выносим отдельно. Приёмка — НЕ байт-идентичность (в отличие от R1-C21: здесь мы намеренно добавляем истинные рёбра): только добавления, каждое прослеживается до видимой в исходнике ссылки, замеренные ложные уходят в ноль, 31 действительно несослатый кандидат остаётся. Доки: гэпgaps/dead_code_high_band_2026-08-24.md, дизайнdocs/design/source_visible_references.md(D1–D5 закрыты: да / да / да / грейдер не трогаем / additions-only), пользовательские —docs/dead-code.md§What counts as a reference. Приёмка выполнена:high46→29 (codemap) и 5→2 (bquant) — ровно те 31, что аудит назвал настоящими; только добавления (bquant +364 пары, 0 удалений; у codemap 1 удаление — реброadd_behavior→_named_functions, вызов которого эта же правка и заменила); все 278 name-ссылок на bquant проверены на присутствие имени цели в исходнике; дублей пар нет. +14 тестов (фикстураrefpkg), сьют 401→415. Три вещи, которых дизайн не предвидел: ① большинство name-load'ов — аннотации, не диспетч (278 против прогноза 149); помечены отдельноresolution="annotation"(213) vs"name"(80), потому что это разные смыслы — «символ исполняется» и «символ в контракте»; ② страх по D3 оказался фантомом: из 5202 вложенных сайтов внутренне резолвятся 279, а новых пар всего 21 (codemap 4) — дорогим было число сайтов, а не рёбер; ③ эталон бенчмарка точности устарел —c10_closureдержалexpected: [], записав старое ограничение как правильный ответ, хотя docstring кейса сам называлouter→helperразрешимым ребром; исправлен наdecidable, deep recall-overall 58%→62.5% при precision 100%. Плюс сплайс R1-C9 пришлось научить новым рёбрам — иначе инкремент их молча терял (поймал его собственный байт-идентичный тест). Смыкается с R1-C8 (градация), R1-C20 (тот же выбор: моделировать связь, а не чинить отчёт), R1-C13, R1-C9. -
R1-C22-f1 (dead-code) локальная переменная, затеняющая функцию модуля, считалась ссылкой ✅ (2026-08-24, без схемы) — issue #9, зеркало #7: там была недо-атрибуция, здесь пере-атрибуция. Автор сперва подтвердил R1-C22 на своём репо — 10 из 10 верно (было 9 из 10 неверно), плюс проверил два способа промахнуться (имя только в докстринге/строке и имя только в Store — ссылок не создают), 1248 кросс-рутовых рёбер, 0 ложных. Гэп: Load локально связанного имени, совпадающего с функцией модуля, атрибутировался этой функции — Python связывает на область, а не на инструкцию, поэтому
_x = 1делает все чтения_xв функции локальными. Мёртвая функция пряталась вlowвместоhigh(--min-confidence highеё не видит; ничего живого не удаляется). Шире, чем в issue: ложные рёбра давали шесть форм — присваивание, параметр,for-таргет,with ... as,except ... asи вложенныйdefтого же имени; параметры выглядели корректными у автора лишь потому, что имя не совпадало. Сделано:_local_bindingsна область функции;global/nonlocalвозвращают имя модулю; модульная область не фильтруется (там ре-биндинг — тот же символ). Своя же ловушка, пойманная замером: первый вариант считал теневыми и функция-локальные импорты — и уронил настоящее ребро на bquant (register_builtin_indicators → IndicatorFactory, импорт внутри тела); импорт связывает имя с той самой целью, которую ребро и фиксирует, поэтому подавлять его нельзя. Приёмка: шесть форм не дают рёбер и остаютсяhigh,globalребро сохраняет, диспетч-таблица уровня модуля не тронута; 0 подавленных рёбер на codemap и bquant. +10 тестов, сьют 415→425. Замечание по методике: bquant в этот момент активно правился соседним агентом (файлы исчезали между чтениями), поэтому сравнения «до/после» на нём делались только вплотную по времени, а детерминизм проверялся на замороженном снимке — на нём граф байт-идентичен. -
R1-C23 (ось B2) робастность на трудном Python — симлинк превращает 17 файлов в 615 модулей ✅ ✅ (2026-08-25, без своей схемы — едет на 0.12 от R1-C25) — замерено, спроектировано и построено в тот же день. Не репорт извне, а упреждающий замер последней открытой оси: именно B2 в прошлый раз отдала issues #4 / #5 в первые 20 минут первой чужой сборки. Проба — пакет на 17 файлов (2814 байт, 157 loc), по модулю на конструкцию: метакласс +
__init_subclass__,type()-класс, модульный__getattr__, monkeypatch, условные импорты, PEP 695 (def f[T],class C[T],type X = …),match/walrus, async,Protocol,@overload,functools.wraps,singledispatch, star-импорт, взаимные импорты,.pyi, BOM, пустой файл, синтаксическая ошибка, latin-1, симлинк каталога на самого себя. Сборка: exit 0, ни одного предупреждения. Сначала — что держится (честность в обе стороны): метаклассы,type()-класс (записан как attribute, а не выдуман классом), PEP 562, monkeypatch, PEP 695-генерики,match, async,singledispatch, взаимные импорты, BOM, пустой файл,.pyрядом с.pyi— всё корректно, иdead-code highпуст — ни одного ложного уверенного утверждения. Значит это не переписывание экстрактора, а пять узких дыр. Замерено: ① симлинк-циклloop → .→ 615 модулей вместо 15 (600 фантомных, 98%), 2378 узлов / 3771 ребро вместо 58 / 91, вложенность до глубины 40, молча; при этомcodemap scopeна том же дереве отвечаетfiles: 17— две половины одного тула расходятся в 36 раз, и никто их не сравнивает; ② нечитаемый файл просто исчезает —broken.py(синтаксис) иlatin1.py(не-UTF-8) отсутствуют в графе без единого слова: тот же класс, что #5 — отчёт о дереве, которое не прочитали, отрендерен как справка о здоровье; ③from X import *не даётimports-ребра вообще → зависимость невидима дляarchitecture(слои, циклы) иcheck; ④ строковые /TYPE_CHECKINGаннотации невидимы (-> "Base"— ничего,-> Reader— ребро), а это ровно идиома для типов, которые иначе дали бы циклический импорт; ⑤ stub-only модуль подаётся как настоящий код (api.pyiбезapi.py→ функция, которой в рантайме нет). Плюс две честные неполноты «на заметку»: PEP 695type Alias = …не даёт узла; функция-локальныйfrom .x import a_fn+return a_fnне даёт ссылки. План (дизайнdocs/design/hard_python_robustness.md, D1–D7): D1 канонизация пути + visited-set (не запрет симлинков — легальный layout); D2 экстрактор возвращает список пропущенных входов,buildих называет; D3 реброimportsдля star-импорта (точное; раскрытие имён — отдельно, отложено как неизмеренное); D4 парсить строковые аннотации тем же путём, с тем жеresolution="annotation", размер мерить до мержа (урок D3 из R1-C22: 5202 сайта → 21 ребро); D5extras.stub=trueи исключение из dead-code вместо выдумывания; D6 — главное: дериватив-сверка «модулей в графе vs файлов в scope-манифесте», ловит ① не зная про симлинки и ② не зная про синтаксис — закон сохранения, а не эвристика (и на #5 бы сработала). Зависимость снята: D2 и D6 нужен носитель, который путешествует вместе с графом — R1-C25 закрыт 2026-08-25, блокprovenanceв графе есть. Приёмка выполнена: при живом симлинке фикстура даёт 15 модулей вместо 615, ни одного висячего ребра, алиас записан (inputs.aliased_modules), а симлинк на настоящий соседний каталог по-прежнему обходится (запрет симлинков был отвергнут не на словах); оба нечитаемых файла названы на stdout и едут вprovenance.inputs.skippedс причиной (syntax/encoding);imports: star → metaесть, внешнийfrom os.path import *ребра не выдумывает;-> "Base"резолвится с тем жеresolution="annotation"; stub-символы помеченыextras.stubи исключены из dead-code; D6 срабатывает в обе стороны на подделанном счётчике и молчит на codemap, bquant и чистой фикстуре. Замер дельты — на замороженных копиях, между двумя версиями тула (подопытный репо правился соседним агентом по ходу, живое «до/после» было бы бессмысленным): bquant +26 рёбер, 0 удалений — все квотированные аннотации; codemap +3, 0 удалений; число узлов не изменилось нигде. +30 тестов (фикстураtests/fixtures/hardpkg; опасные входы — битый файл, latin-1, рекурсивный симлинк — создаются в tmp-копии, а не лежат в репо), сьют 453→484. Оценка стоимости впервые сошлась, и по скучной причине: star-импортов в обоих реальных пакетах нет вообще, а D1 — no-op на любом дереве без цикла. Ловушка замера, пойманная недоверием, а не аккуратностью: первое «до/после» codemap-на-codemap показало +20 узлов и 6 удалённых рёбер, чего эта правка объяснить не могла — оказалось, «старая» сборка шла с рабочим каталогом = своим worktree, и griffe разрешил имя пакетаcodemapизsys.path(cwd), а не из переданныхsearch_paths, то есть старый тул анализировал собственный исходник, а не замороженную копию; из нейтрального каталога сPYTHONPATHдельта стала +3 и ноль удалений. Стоит записи дважды: это живой пример тезиса R1-C25 (два графа — и по артефактам не различить, какой тул что читал) и намёк, чтоsearch_pathsgriffe не авторитетен (не гнались, но и не забыли). Обратная связь в R1-C25: обе сборки в том сравнении отдали одинаковыйtool.commit— одна из чистого worktree на HEAD, другая из грязного чекаута того же HEAD; провенанс писалdirtyпро таргет и не писал про тул. Теперь пишет. Пользовательские доки:docs/hard-python.md. Гэп:gaps/hard_python_robustness_2026-08-25.md. Приёмка — НЕ байт-идентичность (D3/D4 добавляют истинные рёбра, D1 убирает фиктивные): фикстура даёт 15/58/91 при живом симлинке; оба нечитаемых файла названы на stdout; D6 срабатывает на симлинке и на битом файле и молчит на codemap/bquant/всех фикстурах. Гэп:gaps/hard_python_robustness_2026-08-25.md. -
R1-C24 (ось A10) test-mapping: «какие тесты покрывают X» — ответа нет, а наивный неверен в 82% ✅ ✅ (2026-08-25, без схемы) — замерено, спроектировано и построено в тот же день. Ось в watchlist с 2026-07-30 («реальная задача упёрлась в отсутствие среза символ→тесты»). Замер на самом codemap (
build codemap --consumer tests --mode full, оба тира): 1492 узла, 3585/4117 рёбер, 416 тест-функций, 380 core-функций и классов. ① Прямой ответ пуст для 82%: входящее ребро отtests.*есть у 68/380 (18%) — а это ровно то, что сегодня возвращаютcallers/references_to. Достижимость от тест-функций: 225/380 (59%) fast, 304/380 (80%) deep. Тест зовётextract(),extract()зовёт двести вещей — почти ничто из проверяемого тестом не названо в тесте. ② Fast-тир здесь структурно нежизнеспособен, потому что тесты зовут методы на объекте: входящиеcallsразрешены у 91% свободных функций, но лишь у 21% методов (deep: 92% / 56%).Query.hotspots,Query.references_to,Query.consumersпокрыты реальными тестами и имеют ноль входящих вызовов на fast. ③ Достижимый ответ слишком велик, чтобы им пользоваться: медиана 21 теста на символ, максимум 126 (из 416); только ближняя полоса — медиана 6.5, p90 66. Распределение кратчайшей дистанции: 1 хоп — 63 символа, 2 — 95, 3 — 49, 4 — 64, 5 — 28, 6 — 5. Ранжирование здесь не полировка, а сама фича. ④ Шов pytest-фикстур не смоделирован: 262 из 416 (63%) тест-функций получают предмет параметром, 19 фикстур, 0 рёбер тест→фикстура (17 существующих —containsмодуль→фикстура). Структурно это шов F5/M7 (фабрика+реестр+вызов). Но замеренная цена на этом репо — 0 символов (фикстуры достают подмножество того, что достают тела тестов), то есть шов стоит атрибуции, а не покрытия; и свидетель слабый — у codemap нетconftest.py, худшая форма шва тут не представлена вовсе. ⑤ 41% символов не достижимы ни одним тестом (155/380 fast, 76 deep) — и этому числу пока верить нельзя: большая часть разницы — это ②, а не отсутствие тестов; любая поверхность обязана подавать это как нижнюю границу покрытия, а не как «не покрыто». План (дизайнdocs/design/test_mapping.md, D1–D6): D1 «тест» выводится, не хранится (роль консьюмер-рута + имя по правилу pytest) — фикстура фреймворка не запекается в артефакт, и старые графы получают фичу без пересборки; D2 достижимость с полосами по дистанции, ближняя непустая полоса, жёсткий кап 25 иlogтого, что отрезано (правило «никаких молчаливых капов»), две обязательные пометки — over-set («достиг ≠ проверяет») и нижняя граница («динамика невидима»); эвристики похожести имён отвергнуты — дистанция это факт о графе, похожесть это догадка о намерении; D3 тир — часть ответа, наfastпредупреждение (21% методов — не основание для «вот твои тесты»), но не отказ и не скрытая пересборка; D4 мост фикстур по имени вдоль цепочки conftest, реброreferencesсresolution="fixture"(неcalls— зовёт pytest), перед реализацией замерить на сьюте с conftest, иначе кандидат на отсрочку; D5 поверхностьQuery.tests_for→ optests→codemap tests <symbol>, отдающая pytest node id (tests/x.py::test_y), иначе это упражнение по чтению, а не команда; обратный вопрос («что покрывает этот тест») — тем же индексом, бесплатно. Приёмка — precision/recall противcoverage.py, а не «выглядит разумно» (дисциплина F14/F15): выборка 20 символов, оба тира, публикуем и плохие числа; главный показатель — recall ближней полосы; любой символ с «ноль тестов» сверяется с coverage — ложное «его ничто не покрывает» это тот же аффирматив над слепотой, под который уже выпущено четыре фикса. Сделано:Query.tests_for/covers, serve-opstests/covers(29→31 op, 26→28 MCP), CLIcodemap tests <symbol>, отдающая pytest node id и последней строкой — готовую команду. Приёмка — противcoverage.py(прогон сьюта сdynamic_context = test_function, 484 теста), а не «выглядит разумно». Два решения приняла не я, а замер: ① отсечка по дистанции — это обрыв, а не спад. Точность ближней полосы по дальности: 1 хоп — медиана 1.00 (2 теста), 2 — 1.00 (4), 3 — 1.00 (8), 4 — 0.67 (78 тестов!), 5 — 0.33, 6 — 0.23. На четвёртом хопе обход дотягивается до общей тест-инфраструктуры и начинает отвечать «почти весь сьют» — размер прыгает 8→78 за один шаг. Дефолт 3, взят из таблицы; глубже — по явному запросу и с меткойlow. ② «recall ближней полосы» оказался неверной метрикой приёмки — а дизайн называл её главной. Против истины coverage медиана recall — 0.43 по отвеченным символам (p25 0.17, p75 1.00) и 0.02 по всем покрытым, если считатьunknownнулём. Это не дефект, и разброс объясняет почему: recall проваливается ровно там, где истинное множество шире всего — уGraph.add_edgeоно 151 тест (все, кто исполняет хоть строку), а полоса отдаёт 3, то есть recall 0.02. Отдать 151 — значит отдать сьют. Поэтому и заявка изменилась вместе с числом: фича отвечает не «все тесты, покрывающие X», а «ближайшие к X», и значимым оказалось другое — 93% ответов содержат хотя бы один действительно покрывающий тест при медианной точности 1.00. Recall против исполненного множества опубликован потому, что читатель его всё равно домыслит, а не потому, что он цель. Итог на отсечке 3: ответ получают 57% реально покрытых символов (deep; 43% fast), медианная точность 1.00. Оставшиеся 16% —unknown, никогда не «не покрыто» (пятое применение того же правила: #1risk:"none", #3, #5, #7, R1-C23); доминирующая причина названа, а не оставлена загадкой — метод, вызванный на сконструированном объекте: резолв вызовов в консьюмер-рутах именной, поэтомуEngine().run()не даёт ребра вEngine.runни на одном тире. D4 (шов pytest-фикстур) замерен и намеренно НЕ построен: на bquant — сьют с conftest, 894 теста, 48% берут фикстуру параметром, 68 фикстур, 0 рёбер тест→фикстура — символов, достижимых только через фикстуру, 1 из 1043 (на codemap — 0). Фикстуры зовут те же точки входа, что и тела тестов, значит шов стоит атрибуции, а не покрытия; резолвер цепочки conftest ради 0.1% — это gold-plating против собственного стоп-критерия. Записано, чтобы не переоткрывать; триггер построить — заметно другое число в другом репо. +19 тестов (фикстураtests/fixtures/tmworld— мир с заранее известными дистанциями, чтобы приёмка не ездила вместе с собственным сьютом), сьют 484→503. Бенч воспроизводим:research/bench/test_mapping_accuracy.py(нуженcoverage— измерительный инструмент, не зависимость codemap), так что заявка перезапускается, а не остаётся фразой в доке. Пользовательские доки:docs/test-mapping.md. Гэп:gaps/test_mapping_2026-08-25.md. -
R1-C26
--deepбыл хуже fast: тиры взаимоисключали друг друга ✅ ✅ (2026-08-25, без схемы) — из issue #10, заведённого после того, как автор проверил фиксы R1-C21 на своём таргете: импорт-граф ожил (0→407 рёбер),report impactзаработал, но «0 из 338 in-core вызовов пересекают границу модуля». Гэп — регрессия между тирами:add_behaviorвыбирал резолвер по тиру исключающе (_resolve_jediвместо_resolve), то есть при--deepименной резолвер не спрашивали никогда. Пользователь платит минуту за «лучший» тир и получает меньше, чем по умолчанию — единственное направление отказа, которого нельзя предвидеть. Замерено на его же цели (research/shared, 37 модулей, плоский layout): fast — 487 вызовов, 158 кросс-модульных; deep — 336, 0. Импорт-граф при этом на обоих тирах одинаков: работа R1-C21 держится, ломается именно слой вызовов. Механизм — не «jedi не смог»: jedi резолвитfrom leaf import helperправильно, вleaf.helper; неверен следующий шаг — тестstartswith(pkg + ".")читает имя без префикса пакета как external, а external ребра не даёт. Это тот же дефект, что R1-C21, этажом ниже: границу griffe тогда научили (gsource.module_imports), границу jedi — нет, потому что плоский таргет никто не собирал с--deep. И это не только про плоский layout: на упакованных целях взаимоисключение тоже стоило настоящих рёбер — 5 на codemap, 5 на bquant (в основномself.-вызовы, чей метод живёт на базовом классе). Сделано (дизайнdocs/design/deep_tier_union.md, D1–D5): D1_flat_qualify— зеркалоmodule_importsна границе jedi (не фолбэк: ответ jedi верен, неверна классификация); D2 union вместо замены — фолбэк в именной резолвер, и шире, чем в первом наброске: не только приunresolved, но и когда jedi называет символ, который не является узлом графа (self.x, привязанный к подклассу, тогда как метод на базе) — именно это и давало потери на упакованных целях, потому что soundness-даунгрейд в_process_functionсрабатывал после того, как дешёвый резолвер уже недоступен; D3 наexternalне откатываемся — там jedi вынес суждение, а именная догадка могла бы совпасть с внутренним символом по имени; D4 отделилиunresolvedотexternal. Все три пути вызовов (именованные функции, уровень модуля, вложенные def'ы). Приёмка:calls(deep) ⊇ calls(fast)— fast-only 158→0 на цели автора (кросс-модульные 0→234, больше, чем 158 у fast, — как и должно быть у «глубокого» тира), 5→0 на codemap, 5→1 на bquant, где единственная оставшаяся разница проверена и оказалась уточнением, а не потерей (deep резолвит вызов в метод класса, fast называл модульную функцию) — критерий приёмки честно переформулирован в «ни одно истинное ребро не потеряно». Точность бенчаcallgraph_accuracy— 100% на обоих тирах, без изменений. +9 тестов, сьют 503→512. Ловушка замера (снова): первое «до/после» codemap показало, что fast-only вырос с 1 до 2 — и оба ребра были вызовами_resolve_jedi, который эта же правка перестала звать напрямую; fast-эталон был собран до правки. Тот же урок, что в R1-C25: сравнение заморожено настолько, насколько заморожена его менее замороженная половина. Пользовательские доки:docs/flat-layout.md§The deep tier. Гэп:gaps/deep_tier_regression_2026-08-25.md. -
R1-C27 опечатка в контракте красит CI-гейт в зелёный ✅ (2026-08-27, без схемы) — седьмое применение того же правила (#1
risk:"none", #3, #5, #7, R1-C23, R1-C26), и первое, нацеленное не на исходник мишени, а на собственный конфиг тула. Найдено не обкаткой, а замером готовности к выпуску (веха M20): матрица по Python показала 9 падений на 3.10, и под ними лежал дефект, живущий на всех версиях. Три загрузчика —arch.load_contract,integrations.gate.load_config,serve.audit.load_dead_code_whitelist— сваливаютOSError,ValueErrorиModuleNotFoundErrorв одну ветку и возвращают пустой результат, который вызыватель рендерит как «No[architecture]contract found — nothing to enforce», exit 0.TOMLDecodeErrorнаследуетValueError, значит одна убранная скобка вcodemap.tomlпревращает гейт изexit 2(«14 импортов идут вверх по слоям») в зелёную сборку. Замерено на собственном графе codemap: контракт, который реально нарушается →exit 2; тот же контракт без]→ «нечего проверять»,exit 0. Причёмgate.pyзнал: в его докстринге написано «Uses the stdlibtomllib(3.11+)» — знание жило в комментарии и не дошло ни до метаданных, ни до пользователя. Сделано:codemap/tomlio.py— одна точка чтения (read_toml → (data, error)), три условия разведены;ArchitectureContract.error/IntegrationConfig.error, вайтлист возвращает(items, error);is_empty()не тронут и по-прежнему значит «правил нет» — «не смог прочитать» и «прочитал, там пусто» больше не делят ветку.checkна нечитаемом контракте даёт exit 2, а не новый код: статус отвечает на вопрос «можно ли пропускать пайплайн», и новый код отправил бы нечитаемый контракт в ветку успеха у всех, кто уже написалif rc == 2. Толерантность сохранена — ничто не падает и не заклинивает. Дизайн:docs/design/release_engineering.md(D2). Гэп:gaps/unverified_claims_2026-08-27.md§2. -
R1-C28 лимит — это тоже партиальность, и мы про неё молчим ✅ (2026-08-28, из разбора CodeGraph). Восьмое применение того же правила, и первое, найденное в чужом туле и подтверждённое в своём. Первый замер T2 вернул ровно 20 вызывающих, все
kind:"file"— читалось как «у CodeGraph file-import-модель callers», годная находка в карточку и в матрицу. Ложь:--limitпо умолчанию 20, file-строки идут первыми, дефолт обрезал ответ ровно по линии, искажающей модель (на--limit 500— 79 записей, 58 символьных, наши 57 внутри). Тот же вопрос к себе:search "zone"при дефолтномlimit=50отдаёт 50 из 1259, конверт{"ok": true}— ни тотала, ни маркера, ни эха лимита; и это op открытия (F9, «для агента, который ещё не знает имён»)._PARTIAL_OPSне помогает: он про партиальность резолва, а лимит — независимый второй источник нижней границы (callersпомечен и лимита не имеет,searchлимит имеет и не помечен). Scope: блокlimit {applied, returned, total, truncated}в конверте всегда, когда op принимает лимит (включаяtruncated:false— отсутствие поля само неоднозначно);total:null, если счёт не по карману, но не пропуск; CLI-футер на обрезке; тест, падающий, если новый op получилlimitбез блока. Поверхности:search(50),semantic(10), CLIcodemap semantic --limit.packвне scope (бюджет — намеренный и самоописательный),impact --depth— под вопросом (уже эхом вby_distance/max_distance). Приёмка:searchс превышением даётtruncated:trueи истинныйtotal, без —truncated:falseиtotal == returned. Схему не трогает. Гэп:gaps/limit_truncation_2026-08-28.md. Смежное: R1-C13 (машиночитаемая нижняя граница), карточкаresearch/tools/codegraph.md. Второй апстрим-контакт по тому же разбору (2026-08-28): в их трекере нашлась #1566 — ровно наш дефект резолвера (Map.get→ чужой метод) для TypeScript, с их собственной формулировкой «unresolved лучше, чем уверенно неверное ребро». Новый issue не заводился; отправлен комментарий с питоновским repro и со стыком, которого не было ни в одном треде: цикл-детектор, который просят вывести наружу в #888/#889/#1012, считается по этим же рёбрам. Итог обоих контактов (2026-09-08): автор ответил и починил оба. Наш #1639 закрыт PR #1772 («report callers/callees/query truncation», влит через пять минут после ответа, ребейз заглохшего #1647); #1566 закрыт PR #1790 — резолвер перестал угадывать, когда у встроенного типа нет подходящего метода проекта, с таблицей before/after по четырём фикстурам и девятью негативными регрессами. Ни то, ни другое не выпущено: на npm по-прежнему 1.6.0. Отсюда единственная оставшаяся работа по этому треду — переизмерить (ниже). -
R1-C50 у библиотеки не было точки входа, и более полный граф отвечал пустее ✅ (S) — (2026-09-10, без схемы; заведено мишенью, issue #19, на следующий день после 0.0.16 — проверяя наш же замер из bquant#120). Гэп:
gaps/flow_entry_points_2026-09-10.md, решение:docs/design/flow_reach.md§9.entry_pointsтребовалin_degree == 0по всему графу, поэтому функция, которой входят в пакет, дисквалифицировалась тем, что ею пользуются: 43 вызова, все изtests/examples/scripts/research. Второе следствие тяжелее: на deep цепочка замыкается, вход исчезает, иreport impactна одном дереве и одной версии отвечал «шаг 3» на fast и «ни одна точка входа не доходит» на deep. D7 входящая степень внутри своего корня, D8 вход печатает внешних вызывающих как доказательство публичности, D9 пустота называет свой род — четыре различимых ответа, включая «ближайшая голова на шаге N, перезапустите с--flow-depth N». Нота теперь предупреждает в обе стороны (их формулировка точнее нашей). Приёмка: repo-scoped deep, старое правило — 253 входа, до цели не достаёт ни один на любой глубине (их0 of 252); новое — 302, голова на месте, её внешние вызывающие ровно те 43, четыре пресета на шаге 6. Мутационно: прежнее правило теряет поток на 2, 5 и 50 шагах. На однокорневом графе множество входов не изменилось — проверено тестом. -
R1-C51 фильтр по степени отвечал пустотой, которая не говорила, что она фильтр ✅ (S) — (2026-09-10, без схемы; та же issue #19, попутная находка). Решение:
docs/design/edge_resolution.md§9. Их замер: по 25 целям разворачивания реестра 221 exact / 1 inferred / 49 heuristic, и у 13 из 25 нет ни одного exact-вызывающего — это все методы стратегий.callers(min_confidence="exact")отвечал[]о символах, которые зовут на каждом прогоне. Дефект не в фильтре, а в том, что он не объявлялся — R1-C28 в точности, только про сужение вместо обрезки. Конверт несётfilter {min_confidence, returned, total, dropped, by_grade}всегда, иby_grade— состав несуженного ответа, поэтому пустой ответ читается сам. -
R1-C52
diffсудил API потребителей, а не пакета ✅ (S) — (2026-09-10, без схемы; та же issue #19, их прямой вопрос «задумано ли так»). Решение:docs/api-diff.md.diff_apiфильтровал только поvisibility, поэтому на repo-scoped графе 40 из 47 «added public symbols» у них оказались функциями тестов, и--exit-codeкак релизный гейт управлялся правками тестов. Теперь сравнивается один корень (умолчаниеcore),excludedсчитает непросуженные публичные символы по корням, и отчёт называет это под вердиктом (R1-C30-f2).root=None— сравнить все корни. На однокорневом графе ничего не изменилось, кроме строки области. -
R1-C53 сплошной проход по сужениям: каждое сужение обязано себя называть ✅ (M) — (2026-09-12, из собственного разбора последних двенадцати починок). Повод — не гипотеза, а счёт. Восемь из двенадцати закрытых пунктов R1-C41…R1-C52 — один и тот же дефект: ответ уже, чем выглядит, и молчит об этом.
R1-C28лимит обрезал и не сказал ·R1-C44пустота не назвала свой род ·R1-C30-f2гейт не назвал, чего не судил ·R1-C49-f1правило сработало и не назвало себя ·R1-C39маршрут был, а чего он стоит — нет ·R1-C50пустота потоков не различала два случая ·R1-C51фильтр отвечал пустотой, не объявляя себя ·R1-C52diffмолча судил чужой корень. Четыре из последних пяти нашли потребители, а не мы — значит это не случайность, а систематическая слепота, и чиним мы её по одной штуке за раз, когда кто-нибудь наткнётся. Scope: инвентаризация всех 31 операции и 8 видов отчёта по одному вопросу — что этот ответ сужает и объявляет ли он это. Словарь сужений закрытый:limit(обрезка вычисленного списка),filter(предикат),scope/root(судим один корень),bound(глубина обхода),tier(ответ зависит от fast/deep),edge-class(читаем не все классы рёбер),definition(форма ответа зависит от определения, которое мишени может не подходить — какentry_pointsв R1-C50). На каждый класс сужения — сторож по образцуtests/test_r1c28_limit_envelope.py, который краснеет, когда новая операция заводит сужение без объявления. Не «ещё одна починка», а закрытие механизма. Найдено в первом же проходе (замерено, не предположено):columnsпо умолчанию отдаёт 331 из 1057 ключей (subscripted_only=True— осознанное сужение F15) и не говорит об этом: потребитель видит 331 и не может узнать, что их 1057.export mermaid --scopeрежет диаграмму с 144 строк до 47 без единой пометки.communitiesпо построению судит только кореньcore(на однокорневом графе незаметно: 91 из 91; на repo-scoped — молча).reportфорвардит три сужающих параметра (depth,min_confidence,root) — из них объявлен толькоmin_confidenceв dead-code. Сделано: матрица 31 операции × 7 классов —docs/design/narrowing_audit.md§5, ни одной клетки «применимо и не объявлено». Закрыты четыре найденных замером:columns(блокfilterсbasis/total/dropped),communitiesи список входов (новый блокscopeв конверте:{root, judged, not_judged}),mermaid --scope(строка%% scope:— mermaid её игнорирует при отрисовке и она видна в исходнике). Над-множество получило свою формулировку (_EPISTEMIC_OVER_SET): «нижняя граница» про него — хуже, чем молчание, потому что потребитель обрежет меньше, чем надо. Два сторожа читают исходник операций, оба показаны тому, что обязаны поймать. Сьют 918 → 928, схема не двигалась. Решение, которого проход едва не испортил (D1): первая версия добавилаqueryв_PARTIAL_OPSи сломала правило «отсутствие метки значит, что ответ точен» — досье смешанное, и метка на весь конверт сказала бы, что под сомнением и определение символа. Переобъявление партиальности — тот же дефект, что утаивание, направленный в другую сторону. Смешанный ответ объявляет по полю, и поймал этоtest_structural_ops_have_no_label, написанный месяц назад для R1-C13. Чего проход не покрывает: отчёты объявляют сужения прозой, и прозу никакой сторож с поведением не сверяет. -
R1-C54 цепочку цикла печатала ротация, выбранная хэшем ✅ (S) — (2026-09-12, без схемы; заведено лабораторией, issue #20, при подъёме пина на 0.0.17). Гэп:
gaps/cycle_rotation_nondeterminism_2026-09-12.md, решение:docs/design/deterministic_rendering.md.nx.simple_cyclesвходит в цикл там, где его застал обход множеств, то есть хэши строк;arch.pyсортировал цепочки и это выглядело канонизацией — ключ двигался вместе с ротацией. Периметр оказался шире заявки: под восемью сидами разошлись три поверхности (check,report architecture, и JSONbuild_architecture, который отдаёт MCP-инструмент), а не одна; устойчивы былиreport dependenciesи доки — они печатают счётчики, не цепочки. Поэтому канонизация у источника (query.py, три производителя, один помощник), а не вarch.py: там вылечился бы гейт и остался бы течь машиночитаемый ответ. Приёмка: до — 3 вывода на 8 сидов у каждой из трёх поверхностей; после — 12 сидов, все шесть по одному; на живом дереве с 9 настоящими циклами пять сидов дают один текст. Сторож порождает процессы (PYTHONHASHSEEDчитается при старте), проверяет семь поверхностей включая уже устойчивые, и имеет двойной положительный контроль. Сьют 928 → 938. Цена, названная прямо: дефект подделывался под сдвиг поведения — наша же процедура релиза сверяет вывод двух версий на одном дереве, и заявитель споткнулся об это первым. И это класс, которого R1-C53 не увидел: там спрашивалось, объявлен ли ответ, а не воспроизводим ли он;reproducibilityдобавлен восьмым классом в матрицу сужений. -
R1-C55 грейд
highу переопределений: «нет входящих вызовов» там, где вызов есть (S) — 🆕 (2026-09-12, ось B4, гэпgaps/third_shape_2026-09-12.md§S2). Замерено наPIL: из 63 символов с грейдомhigh— 40 переопределяют метод базового класса, у которого в том же графе записан входящий вызов. Формулировка отчёта — утверждение о факте: «no inbound calls, references, or decorators», и оно неверно:BmpImageFile._openпереопределяетImageFile.ImageFile._open, который зовётImageFile.__init__(self._open()). Шаблонный метод — обычная ООП-форма; на ней 63 % самого уверенного грейда ложны. Данные для понижения уже в графе: 191 реброinherits. Решение: символ, переопределяющий метод предка (транзитивно), не может иметь грейдhigh; если у предка есть входящий вызов — этоlow(«что-то его зовёт через базу»), если предок сам никем не зван —mediumс названной причиной. Приёмка: наPILни одного_open-переопределения вhigh; на bquant/codemap составhighне деградирует; сторож мутационный — убрать правило, тест краснеет (R1-C37: показать проверке ровно ту форму, которую она обязана отвергнуть). Сделано (2026-09-12, без схемы;query._overridden_base+ 5 сторожей вtests/test_r1c55_override_grade.py, сьют 938 → 943). Замерено обеими версиями на одном дереве:PILhigh 63 → 15 (40 ушли вlow«reached by dispatch, not by name», 8 вmedium«dead only if the base is»), codemap 31 → 31, bquant 2 → 2 — ни одно из наших деревьев формы не несёт, поэтому месяц обкатки её и не дал. Обход транзитивный и предпочитает вызываемого предка: в цепочке из трёх звеньев среднее само переопределение, своих входящих вызовов у него нет, и первая версия починки останавливалась на нём и ставилаmediumживому методу — это поймал сторож, до коммита. Мутационная проверка: без правила краснеют 3 из 5 тестов, два положительных контроля остаются зелёными (полосаhighне опустошена). -
R1-C56 что такое
.pyi— один ответ вместо трёх (M) — 🆕 (2026-09-12, ось B4, гэп §S1). Сейчас стаб получает три разных ответа в одном инструменте: экстрактор считает его модулем (griffe подхватывает.pyiрядом), манифест его не видит (scope.DEFAULT_INCLUDE = ("*.py", "*.md")) →scope_idрасходится и идентичность входа объявляетсяunknown, а с ней--incrementalиwatch;dead-codeимеет отдельное правило (R1-C23). Цена замерена: наPILединственный жёсткий import-цикл (ImageFont → _imagingft → ImageFont) собран из строкиfrom . import ImageFontв_imagingft.pyi— стаб не исполняется никогда, цель вообще C-модуль, то есть флагманское «жёстких циклов: 1» ложноположительно на 100 %. Решение (эскиз, до дизайна): импорты из.pyi— не runtime-рёбра (по природе это тот же слой, чтоif TYPE_CHECKING, R1-C48); сами стабы — во входы манифеста; узлы, пришедшие из стаба, несут это вextras, чтобы ответ про C-модуль не выдавал стаб за исполняемый код. Приёмка: наPILжёстких циклов 0,scope_idсовпадает,query _imagingназывает источник. Сделано (2026-09-12, без схемы; дизайнdocs/design/stub_files.md, 11 сторожей вtests/test_r1c56_stub_files.py, сьют 943 → 954). Четыре решения: D1 — импорт из стаба получаетextras.scope = "stub", четвёртое значение, а не заимствованныйtype_checking(механизм другой, а классы циклов названы по механизму); D2 — в виде циклов группируем по последствию, поэтому стабовый цикл попадает в класс «никогда не исполняется», а формулировка класса расширяется; D3 —.pyiвходит вDEFAULT_INCLUDE, и манифест наконец описывает прочитанное; D4 — досье несётstub: true, потому что расширение файла — не ответ. Замерено обеими версиями на замороженных чекаутах:PILжёстких циклов 1 → 0 (цикл переехал в свой класс, 11 → 12), предупреждение про манифест исчезло,scope_idстабилен, изменились ровно 3 ребра при идентичных узлах;attr— узлы и рёбра идентичны, двинулся толькоscope_id;_pytest(78 модулей, стабов нет) — узлы, рёбра иscope_idпобайтово те же, и это контроль обещания D3. Мутационно: выключение D1/D3/D4 роняет 6 сторожей из 11, пять оставшихся — положительные контроли. Цена названа: на дереве со стабами первая пересборка после обновления будет полной, а не инкрементальной. -
R1-C57 пакет-фасад: падение на алиасе и «1 public symbol» (M) — 🆕 (2026-09-12, ось B4, §E1/E10). Форма «публичный пакет отдельно от реализации» (
pytest/_pytest,attrs/attr) ломает три вещи сразу. (1) Падение:codemap build attrs/src/attrs→ код выхода 1,error: Could not resolve alias attrs.field pointing at attr.field. Нерезолвимый алиас — это партиальность, а не аварийная ситуация: его надо пометить и продолжить. (2) Уверенно неверный ответ:report api-surfaceна фасадеpytestпечатает «1 public symbols across 1 modules» там, где__init__.pyпереэкспортирует 90 имён. (3) Предупреждение не доезжает: «0 import edges … read as unknown, not a clean bill of health» звучит наbuild(stderr) и в отчёт не попадает. Приёмка: сборка фасада завершается; отчёт называет долю имён, чьё определение лежит вне корня; предупреждение видно в самом отчёте. Сделано (2026-09-12, без схемы; 7 сторожей вtests/test_r1c57_facade_package.py, сьют 954 → 961). D1: падение шло из самогоgriffe.load— слияние.pyiрезолвит алиасы, а алиас в незагруженный пакет бросаетAliasResolutionError. Теперь сиблинг, на который указывает алиас, подгружается в ту же коллекцию (до 8 штук) и загрузка повторяется; если сиблинга на пути нет — наша ошибка с названием пакета и что делать, вместо чужого трейсбека. Сиблинг в граф не попадает: обход стартует от своего корня (проверено — уattrsвсе узлы подattrs). D2: публичный алиас на внешнее определение — это и есть API пакета; теперь он даёт реброexportсextras.external = true, аimport numpy as np(непубличный) по-прежнему не даёт ничего. D3:api-surfaceпонесла диагностики сборки — «0 import edges … read as unknown» доезжает до того самого отчёта, который про фасад и спрашивают. Замерено:attrs/src/attrsсобирается (42 узла, 41 ребро, все подattrs); фасадpytest— 88 внешних переэкспортов вместо нуля, и отчёт печатает «1 public symbols across 1 modules; 88 more re-exported from outside this root» с оговоркой «not judged here»; на codemap и bquant внешних переэкспортов 0 — ни одного нового ребра. Мутационно: выключение D1 роняет весь файл (фикстура несёт@overloadв стабе поверх алиаса — ровно формаattrs/__init__.pyi), D2 и D3 — по своим сторожам. -
R1-C58 определения, которые перестают значить своё при смене формы (M) — 🆕 (2026-09-12, ось B4, §S3/S4). Два замера на
_pytest(78 модулей, сопоставимо с bquant 92 и codemap 52): 1080 жёстких / 95 001 ленивых / 464 109 type-only циклов против 0/9/0 и 0/0/0 на прежних деревьях; отчёт — 1632 строки и 10.3 с, из которых почти всё — перечисление циклов плотного клубка ради печати двадцати. И «Layers (105)» у плоскогоPIL: слой = первый сегмент пути, подпакетов нет, поэтому каждый модуль — свой слой, и это подаётся как обзор архитектуры. Оба — классdefinitionиз матрицы R1-C53, необъявленный. Решение: для клубка отвечать не количеством циклов, а составом SCC («N модулей в одном взаимно-связном клубке, вот они»), с количеством как производной величиной и объявленной ценой; для слоёв — объявлять, что при плоской раскладке «слой = модуль», и не выдавать это за структуру. Сюда же консистентность списков: два менее severe списка обрезаются на 20 и объявляют это, а самый severe печатается целиком (1080 строк). Сделано (2026-09-12, без схемы; дизайнdocs/design/cycle_tangles.md, 11 сторожей вtests/test_r1c58_cycle_tangles.py, сьют 961 → 972). Единица ответа — клубок (компонента сильной связности) с одним примером цикла, ребромclosed_by, на котором он держится, и рангом (E − V + 1) — числом независимых петель. Ранг добавлен не сразу: первая версия выбросила свойство, которое пинил сторож R1-C29 из issue #11 («две петли, делящие модуль, — две проблемы»), и сторож это поймал. Замерено:_pytest1080/95 001/464 109 → 1 клубок, 19 модулей, 57 петель (и 32/130, 33/153), отчёт 1632 → 137 строк, 10.35 с → 0.40 с; гейт — одна строка вместо 1080. На малых клубках новое число совпадает со старым: bquant 9 циклов → 9 петель, attr 10 → 10; расходятся там, где старое перестаёт значить (1080 → 57). Плоский пакет теперь объявляет, что «слой = модуль», и не печатает межслойный вид, дословно повторяющий import-граф; вложенный пакет (контроль) сохраняет прежний вид. Мутационно: выключение D1 или D4 роняет 7 сторожей из 11. -
R1-C59 просадка полноты графа между сборками одного дерева не замечается (M) — 🆕 (2026-09-12, разбор R2.2
rag_for_git, карточкаresearch/tools/rag_for_git.md§0.4 S4). У нихreviewer indexчитает предыдущие счётчики рёбер по типу отношения до перестройки и печатает предупреждение, когда счёт по типу упал на той же ветке (reviewer/graph/metrics.py:detect_edge_regression, их бриф PRI-252). У нас нет ничего, что заметит «эта сборка того же дерева зарезолвила на 30 % меньше вызовов, чем предыдущая»: есть provenance, есть версия схемы, есть два тира — и нет замера полноты. Ровно эта форма дефекта разбиралась в post 04: два билда одного дерева, 30 рёбер против 38, и обе сборки объявляют одну и ту же схему. Тогда починка (R1-C25) научила артефакт называть вход; она не научила его сравнивать полноту. Эскиз: класть вprovenanceсчётчики по типу ребра (и, отдельно, поresolution/scope), аdiffи--incremental— сравнивать их и объявлять просадку. Осторожно с R1-C42: на deep-тире одна сборка — это выборка, поэтому порог обязан быть выше шума выборки, иначе сторож будет врать на каждом втором прогоне. Приёмка: на зафиксированном дереве две сборки разными версиями дают явное сообщение о просадке; на одинаковых сборках — тишина; на deep-тире шум выборки не поднимает флаг. -
R1-C60 вложенные определения не узлы, и это не объявлено (S) — 🆕 (2026-09-12, тот же разбор, §0.4 P3). Их id
path#fqnпереводится в наш точечный id механически, и 2304 из 2354 их узлов попадают в наши. Не попадают 50 — все вложенные определения:cached.decorator.wrapper,_apply_modular_config.ModuleLevelFilter, шестьregister_*.decorator. Проверено: у нас их нет ни под каким именем (decorator— 0 узлов,<locals>— 0; виды узлов: function/column/attribute/ class/module/doc). То есть функция, определённая внутри функции, для codemap не существует, и ответ об этом не говорит — классdefinitionиз матрицы R1-C53, необъявленный. Цена реальная: декоратор, объявленный внутри метода-регистратора, — обычная форма реестра плагинов, иdead-codeпро него молчит вместо «не сужал». Решение (до дизайна): либо эмитить вложенные определения узлами (тогда думать про id и про то, что они делают сdead-code), либо объявить сужение в ответе. Второе дешевле и обязательно в любом случае. Приёмка: на bquant@cb89a24 те же 50 символов либо в графе, либо названы в объявленном сужении; сторож мутационный. -
R1-C61
packизмерен на детерминизм и на бюджет, но не на полезность (L, дверь с названной ценой) — 🆕 (2026-09-16, интейк R1.6research/06_multimodal_repo_views.md, повод — RepoAtlas, arXiv 2609.16936v1). Мы измерили, что ранжированный срез влезает в объявленный бюджет, что ранг воспроизводим и что ответ несёт метку сужения. Мы никогда не измеряли, что агент с нашимpackрешает задачу лучше, чем без него. Единственное основание утверждать пользу — «механизм разумный», то есть ровно тот стиль рассуждения, который проект отвергает во всём остальном. Снаружи такое сравнение уже публикуют: PPR-отбор под бюджетом 15 узлов / 20 рёбер, метрика — resolve rate на SWE-bench Verified, заявлено +2.4 п.п. и −5.8 % входных токенов против сильнейшего мультимодального бейзлайна. Почему это дверь, а не гэп. Такой замер ставит модель в контур: результат зависит от модели, харнесса, промпта и сида, и читатель не воспроизведёт его без тех же моделей. Это было бы первое утверждение в проекте, которое нельзя перевывести из замороженного дерева, — и цену надо признать до, а не после. Третье независимое свидетельство PPR (aider → HippoRAG 2 → RepoAtlas) саму механику подтверждает; под вопросом польза, а не выбор алгоритма. Если открывать — форма, которая не врёт: (1) фиксированный набор задач и дерево, объявленные как выборка, а не как бенчмарк; (2) A/B на одном харнессе, где меняется только наличиеpack; (3) несколько прогонов с разбросом, иначе дельта в 1 п.п. неотличима от шума (на 500 инстансах 1 п.п. ≈ 5 задач); (4) в отчёте — и токены, и вызовы, и решённое, потому что пакет может экономить бюджет и терять решения; (5) прямая оговорка, что число не воспроизводимо без тех же моделей — иначе оно поедет в positioning как обычный наш замер, а оно другого рода. Пока не открыто. -
R1-C62 правило контракта, которое не может сработать, считается выполненным (M) — 🆕 (2026-09-26, интейк
research/05_curated_sources.md§4a — чужой репозиторий про устаревающие требования, Emmimal/intent-continuity, прогнан у нас). Замерено на фикстуре из двух слоёв (api,core): контракт, в котором все имена вымышлены —layers = ["frontend", "middleware", "persistence"],independent = [["frontend","middleware"]],forbidden = [{from="middleware", to="persistence"}]— даёт✅ Contract satisfied. Rules enforced: layered (3), independent (1), forbidden (1), no_cycles, код выхода 0. Шесть правил объявлены выполненными, ни одно не может сработать.exhaustive = trueловит это только с другой стороны (сообщает о необъявленных слоях кода, exit 2) и по умолчанию выключен. Почему это дефект, а не придирка: переименуйте слой и забудьте проcodemap.toml— гейт продолжает печатать ✅, не проверяя больше ничего. Это тот же класс, что R1-C37 («проверка, которой ни разу не показали то, что она обязана отвергнуть, — не проверка»), только применённый к самому контракту, и та же семья, что R1-C28: правило, ничего не покрывшее, — это партиальность, и она обязана быть названа. Идея вопроса пришла снаружи: их вклад — шаг верификации («не устарело ли требование»), которого нет ни у нас, ни у трёх architecture-as-tests реализаций из §4. Чего решение НЕ должно делать: падать на любом неизвестном имени. Слой может быть запланирован и пуст, контракт может быть общим для нескольких пакетов, аforbidden— страховкой на будущее. Такое ужесточение сломает законные конфигурации и будет хуже болезни. Форма решения: ответ называет вакуумные правила — «layered: 3 объявлено, 1 применимо;middleware,persistenceв графе отсутствуют» — и отдельный опт-ин (strict_names = trueили расширениеexhaustive) превращает это в нарушение. Приёмка: на фикстуре выше ✅ больше не печатается без оговорки; контракт, где все имена настоящие, в выводе не меняется ни на байт (контроль); сторож мутационный — убрать подсчёт применимости, тест краснеет. Сделано (2026-09-26, без схемы; дизайнdocs/design/vacuous_contract_rules.md, 11 сторожей вtests/test_r1c62_phantom_rules.py, сьют 972 → 983). Семантика не тронута: фантомные правила по-прежнему инертны, потому что писать контракт вперёд кода — документированное свойство. Изменился отчёт: счёт несёт два числа, когда они различаются (layered (3 declared, 0 applicable)), отсутствующие имена названы строкой,applicabilityесть в структурном ответе всегда. Опт-инno_phantom_rules = trueделает фантом нарушением (зеркалоexhaustive, по умолчанию выключен). Замерено против 0.0.20 из PyPI: на фикстуре со всеми вымышленными именами появляется раскрытие и, с флагом, exit 2 с тремя именами; на здоровом контракте и на живом дереве потребителя вывод побайтово идентичен — то есть у соседей текст не двинулся. -
R1-C63 у формы ответа нет версии, хотя её диффают потребители (M) — 🆕 (2026-09-26). Замерено по коду:
SCHEMA_VERSION(сейчас 0.13, не менялась девять релизов) версионирует артефакт; у рендеренных ответов — markdown-отчётов, строкsummaryу нарушений гейта, человекочитаемых блоков в конвертах — нет ни одной версии (grep поreport_version,render_version,output_version,text_version— ноль совпадений). Почему это контракт, а не косметика. В 0.0.20 байты графа не изменились вообще, а текст архитектурного отчёта и вывод гейта изменились радикально (единица ответа про циклы стала клубком):## Import cycles: N+ N строк →1 tangle(s), M module(s), K independent loop(s)+ состав. Гейт лаборатории диффает текст, и один раз они уже завели текстовое расхождение как дефект (#20, вращение цепочки цикла — и были правы). В 0.0.20 от повторения спасло только то, что я написал им предупреждение руками: «это настоящее изменение формы, вердикт не сдвинется, диффа будет много». Механизма нет; есть моя внимательность, а она не сторож. Решение (эскиз): отдельныйANSWER_FORMATрядом соSCHEMA_VERSION, независимый от него (в том и суть: граф может быть побайтово тот же), видимый в самом ответе — в шапке markdown-отчётов и в конверте serve/MCP, — и повышаемый, когда меняется структура, видимая потребителю: появился/исчез раздел, изменился смысл считаемой единицы, изменилась форма строки вердикта. Правило повышения выписать текстом, иначе оно сгниёт. Чем это НЕ должно быть: (1) вторым номером схемы, приклеенным к графу — граф здесь не меняется; (2) обещанием текстовой неизменности внутри версии: опечатки и формулировки правим свободно, иначе версия начнёт мешать чинить прозу; (3) счётчиком, который щёлкает на каждый правленый пробел — такая версия шумит и её перестают читать, а это хуже отсутствующей. Приёмка: на замороженном дереве (tests/frozen.py— он для этого и есть) сторож рендерит отчёты и гейт, сверяет хэш с пином рядом с объявленнымANSWER_FORMATи при расхождении говорит прямо: «поднимиANSWER_FORMATили это регрессия». Две версии codemap на одном дереве: граф побайтово один,ANSWER_FORMATразный — это и есть то, что потребитель должен уметь прочитать, не дожидаясь письма. Плюс запись в процедуру релиза: смена формы ответа упоминается в ноте отдельной строкой, как сейчас упоминается схема. Связано: R1-C62 (вакуумное правило контракта) — обе починки меняют вывод гейта, поэтому соседям (bquant #120, bquearch #5) писать одним поводом, с замером на их же деревьях: на сегодня контракт bquant честен — 7 объявленных слоёв, все 7 существуют,exhaustive = true,✅ layered (7), forbidden (8), exit 0, — то есть у них изменится текст, а не вердикт. Сделано (2026-09-26, схема не тронута; дизайнdocs/design/answer_format_version.md, 7 сторожей вtests/test_r1c63_answer_format.py, сьют 983 → 990).ANSWER_FORMAT = 1вcodemap/model.pyс правилом повышения рядом; трейлер_answer format 1 · schema 0.13_у CLI-markdown одним швом, полеanswer_formatв каждом конверте serve/MCP,--format jsonчист. Сторож — пин хэша по шести отрендеренным поверхностям на фикстуре, принадлежащей тесту (пин по живой мишени двигался бы от чужих коммитов — R1-C25); он не решает, структурное ли изменение, а не даёт ему пройти молча, и печатает все три прочтения. Замерено против 0.0.20: вывод гейта получает ровно одну строку (diff 5a6), узлы и рёбра графа хэшируются идентично, различается толькоprovenance.tool; пин стабилен подPYTHONHASHSEED0/1/7/42/99. Цена названа: трейлер один раз меняет текст у всех. -
R2-codegraph: переизмерить после релиза (S) — 🆕 (2026-09-08). Все наши опубликованные числа по CodeGraph (136 циклов против 41 настоящих, precision 10 %, молчаливое усечение по
--limit 20) относятся к 1.6.0, а четыре починки лежат вmainневыпущенными. Как только выйдет релиз: прогнать те же три замера на том же дереве, обновить карточку иresearch/comparison.md, и одним комментарием отдать автору результат по нашей же добавке в #1566 — падает ли число ложных циклов на 207-файловом пакете с 136 до единицы. Публиковать цифры починенного дефекта нельзя; это то же самое, в чём мы упрекали их усечение. Сделано (codemap/serve/limits.py+tests/test_r1c28_limit_envelope.py, 21 тест): блокlimit {applied, returned, total, truncated}в конверте всегда, когда op принимает лимит —search,semantic,tests,covers, плюс MCP-кэпыimpact/call_contract.searchтеперь считает полный матч-сет и режет в serve-слое (Query.search(limit=None)), поэтомуtotal— настоящий, а не размер страницы. Три решения по ходу, которых не было в scope: (1)semantic— честныйtotal: null: лимит применяет сам внешний адаптер, и когда он вернул полную страницу, доцензурный тотал не наблюдаем — так и сказано (total: null,truncated: null,note), вместо того чтобы выдать число, которого никто не считал; счёт поraw, а не поhits, потому что дедуп двух чанков в один символ — это не потеря от лимита; (2) MCP-слой сведён к тому же словарю — там уже был свой (shown/total, только при обрезке); теперь один конверт на всех поверхностях,_compact_impactпечатаетrefs_shown/refs_totalна каждой записи, а не только на урезанных; (3)tests/coversтоже получили блок, хотя они и раньше говорили правду в теле (total_at_distance+ caveat): машинный потребитель должен читать один словарь, а не диалект на op. Исключения записаны письменно (_UNLIMITED_BY_DESIGN:pack— бюджет намеренный и самоописательный;impact/flows— depth ограничивает обход, а не режет посчитанный список), и сторожевой тест читает исходники ops: op с limit-подобным аргументом обязан быть либо в_LIMITED_OPS, либо в исключениях с причиной — иначе падение. Док:docs/accuracy.md§(c). -
R1-C29 импорт-карта только модульного уровня, а
architectureназывает это свойством ✅ (M) — (2026-08-28, issue #11 со второй реальной мишени, через несколько часов после R1-C28). Ограничение экстрактора записано давно (gaps/hard_python_robustness_2026-08-25.md); не записано, что оно делает с потребителями:report architectureпри пустом списке печатает_none — import graph is acyclic._— утверждение о свойстве поверх частичной карты. У репортёра: 47 модулей, 29 пар связаны только функционально-локальным импортом (26%), отчёт — 0 циклов, AST по всем импортам — 2, и оба закрыты локальным импортом. Формулировка репортёра, которую стоит запомнить: локальный импорт — это то, чем разрывают цикл, значит невидимые рёбра — ровно те, что вероятнее всего его замыкают; слепое пятно антикоррелирует с вопросом. Воспроизведено на 3 файлах: в одном операторе вызов резолвится, импорт — нет. И это наша мишень тоже: наbquant35 из 266 импортов (13%) локальные, мы рапортуем 1 цикл при девяти — recall 11%, и это число публиковалось как доказательство точности. Пересчёт сравнения с CodeGraph по общей истине: мы 100%/11% (precision/recall), они 4%/67%. Scope: (1) счётчикimport_map {module_level, function_local_skipped}вarchitecture/check/dependencies— всегда, включая ноль, тем же правилом, чтоlimit(R1-C28); (2) убрать аффирмативные формулировки (architecture.py:65,livingdocs.py:154) — девятое применение «unknown ≠ none» и первое, где уверенный ответ это свойство безопасности; (3) затем резолвить локальные импорты в карту, помечая ленивость (они не равны модульным: одно исполняется на импорте, другое нет). Приёмка и границы:gaps/import_map_module_level_2026-08-28.md§6–§7. Сделано в тот же день, все три уровня (tests/test_r1c29_lazy_imports.py, 12 тестов). Числа пришлось исправлять дважды, и второй раз — из-за нас: «9 циклов» было получено пятнадцатиминутным AST-сканом, написанным для проверки инструмента, а он якорил относительный импорт внутри__init__.pyпакета на родителе вместо самого пакета. Правильное число — 41 (recall был 2.4%, а не 11%; у CodeGraph 32%/10%, а не 67%/4%). Скрипт-аудитор оказался менее аккуратным, чем аудируемый инструмент. После починки набор циклов инструмента множественно совпадает с независимым сканом (41 из 41, те же множества) — это и есть приёмка. Два решения, которых не было в дизайне: (а) цикл теперь двух видов —import_cycles()остаётся eager-графом (ленивый импорт это лекарство от import-order-поломки, и считать его циклом значило бы назвать лекарство болезнью), а замыкающиеся только через ленивый импорт идут отдельной секцией «не падает на импорте, но модули неразделимы»: на bquant это 1 и 40; (б) импорт в теле класса — eager, он исполняется на импорте; griffe не записывает ни тот, ни другой (измерено). Всё, кроме циклов — coupling, слои, dependents, orphans — считается по полной карте. Поведенческое изменение: нарушения слоёв теперь видят ленивые импорты, и на bquant сразу всплылоanalysis ↔ core, которого отчёт не показывал: единственное обратное ребро написано внутри функции. Гейт, который обходится тем, что импорт делают ленивым, — не гейт. Подтверждено репортёром на его дереве (гэп §8): 84 → 113 внутриядровыхimports-рёбер, 29 с меткойscope: "function", пропущенных пар стало 0 из 29. Причём codemap нашёл три ленивых цикла там, где issue заявлял два: их скан собирал DFS-back-edges вместо перечисления простых циклов, и трёхузловой цикл исчезал. Итого за один день два независимых скрипта-аудитора оказались менее аккуратны, чем аудируемый инструмент, в противоположные стороны (мы недосчитали 41→9, они 3→2). Форма их цикла закреплена регрессионным тестом. -
R1-C30 быстрый тир не резолвит вызовы через функционально-локальный импорт ✅ (2026-08-29, остаток от R1-C29 / issue #11). R1-C29 закрыл карту импортов; резолв вызовов остался как был. Измерено на трёхфайловом примере из issue:
def go(): from leaf import helper; return helper(x)— на--deepреброcalls go -> leaf.helperесть, на fast его нет. То есть асимметрия, которую репортёр назвал доказательством «дело не в поддержке, а в карте», верна для deep и неверна для fast: там слепы обе стороны. Экономически это заметно: fast — дефолтный тир и тот, что крутится в петлеcodemap watch. Scope:behavior.pyуже парсит AST каждого модуля и получаетmodule_imports(mod, ...)— добавить импорты, видимые в теле обрабатываемой функции. Ключевое ограничение дизайна: карта должна быть пофункциональной, а не слитой в модульную — локальный импорт в функции A не должен резолвить имя, использованное в функции B, иначе мы купим recall ценой ровно тех ложных рёбер, за которые критикуем CodeGraph (dict.get→ чужой метод). Приёмка: пример из issue даёт реброcallsна обоих тирах; тест, что имя, импортированное локально в одной функции, не резолвится в другой; счётчикcalls-рёбер на догфуде не растёт за счёт ложных (сверка с deep-тиром как эталоном). Мишень для замера: дерево репортёра — 29 локальных импортов на 47 модулей, плоский layout. Сделано (гэп §10): карта строится по функции и наследуется лексически — вложенная функция видит импорт объемлющей, тело класса не видно в методах (правило Python, не удобства). Проверено против deep-сборки с предыдущего коммита — ответ jedi, посчитанный кодом, который эта правка не трогала:calls962 → 986 на bquant и 442 → 502 на codemap, +84, из них 84 подтверждены, 0 потеряно; разрыв deep∖fast 600 → 576 и 225 → 165. Микро-сьют получилc11_local_import, где соседняя функция зовёт то же имя, ничего не импортировав, — дешёвый способ поднять recall (слить карту в модульную) проявляется там потерей precision, а не лучшим счётом; recall по всем истинным рёбрам fast 57.1% → 64.7%, deep 60.0% → 66.7%. Цена: лишнего парса нет (AST уже разобран пассом), гейт на отступ передimportобщий с §9 — 3.3% поведенческого прохода на bquant, 4.1% на codemap. Не покрыто:import pkg.leaf+pkg.leaf.other()(приёмник — цепочка атрибутов; ограничение fast-резолвера и на модульном уровне), локальныйimport *. -
0.0.5 опубликован ✅ (2026-08-31) —
codmap0.0.5, схема 0.13 (без изменений). Релиз корректности, а не возможностей: R1-C33 (объявленная сигнатура в досье), R1-C34 (рендер терял вид параметра — 14.3% функций codemap, 16.3% bquant, и это тихо выключило три правилаapidiff), R1-C35 (гейт не говорил, где искал контракт), R1-C36 (сборка могла анализировать другой пакет с тем же именем — выход 0 и полный граф не того кода). Общее у всех четырёх: тул отвечал, ответ был правильной формы, и проверить его было нечем. Три найдены при работе над другим, один прислала лаба. Процедура та же (чистое дерево на запушенном9523dc1→uv build→twine checkPASSED → upload → тегv0.0.5), а проверка выбрана под то, что релиз чинит: чистый venv вне чекаута и сборка из каталога, где лежит пакет с тем же именем, что у цели — то самое условие, при котором 0.0.4 отвечает о чужом коде. На опубликованном колесе: граф описывает переданный каталог и не содержит ничего из затеняющего, абсолютных путей нет (D5), сигнатура приезжает какrun(target: str, *args, verbose: bool = False, **opts) -> bool, досье класса несётconstructor: __init__(self, name: str, *, retries: int = 3) -> None, аcheckназывает абсолютныйcodemap.toml, в котором искал, и говорит, что промах — это выход 0. Шаг, который стоит записать: бампpyproject.tomlуронилtest_the_declared_version_matches_pyproject— editable-установка кеширует метаданные и всё ещё сообщала 0.0.4. Это охранник, заведённый в 0.0.4, работает ровно как задуман; лечитсяuv pip install -e .. Процедура, пропускающая переустановку, выпустила бы пакет, метаданные которого расходятся с его же исходником. -
0.0.4 опубликован ✅ (2026-08-29) —
codmap0.0.4, схема 0.13. Девять записей Fixed с 0.0.3 (двухдневной давности), и все, кроме одной, пришли с чужого дерева: R1-C28, R1-C29, R1-C30 + f1 + f2, R1-C31, R1-C32, M3.2 + f1. Схема — причина, по которой релиз не косметический: repo-scoped граф теперь имеет одно начало координат для путей, и всякий, кто ставит с PyPI, иначе собирает старую семантику. Процедура та же (чистое дерево на запушенном45de657→uv build→twine checkPASSED → upload → проверка из PyPI в чистом venv вне чекаута: схема 0.13,provenance.tool={"name": "codemap", "version": "0.0.4"}без коммита) — и вдобавок проверены на собранном графе, а не приняты на веру, две вещи, ради которых релиз и делался: вызов через локальный импорт к реэкспортированному имени резолвится на быстром тире, и у символа под корнем-потребителем естьfile. Тегv0.0.4. Своя находка релиза:codemap/__init__.pyобъявлял__version__ = "0.0.2", пока на PyPI лежал 0.0.3 — литерал, отставший на релиз и штамповавший неверную версию в каждый SCIP-индекс и ctags-файл. Графы не пострадали, и причина показательна: провенанс спрашиваетimportlib.metadata, а не читает литерал, — то есть единственное место, где число обязано быть верным, к неверной копии не обращалось. Теперь источник один, и его держат два теста. Версия в двух файлах — та же форма, что скоуп в двух файлах (M19) или дебаунс со своим представлением об изменении (M3.2-f1). -
R1-C32
report --format jsonотдавал граф вместо отчёта ✅ (2026-08-29, issue #14). Ветка формата стояла до диспетчеризации поargs.kind, так что вид отчёта в json-пути не читался вовсе: три разных вида давали побайтово одинаковый вывод — весь граф. Репортёр наткнулся, беряapi-surfaceдля замера покрытия документации, и сначала списал на свою ошибку в вызове. Его же формулировка объясняет, почему это дефект, а не отсутствующая фича: «отказа нет, есть подмена: хорошо оформленный не тот ответ… молчаливая выдача графа хуже отказа: она проходит проверку „ответ получен“». То же семейство, что R1-C30-f2 и R1-C31, — ответ формы правильного, из-за которой читатель не проверяет. Сделано: структурная форма у каждого вида, который принимает CLI — новыеbuild_api_surface,build_dependencies,build_dead_code,build_behaviorрядом со своими рендерерами (плюс уже существовавшийbuild_architecture),impact/flows/communitiesчерез методыQuery. Содержимое то же, что у markdown, на уровень глубже:api-surfaceотдаёт символы с сигнатурой, докстрокой и файлом;dependenciesдержит эагерные и ленивые циклы раздельно (различение R1-C29 доживает до данных, а не только до печати);dead-codeнесёт градацию иwhitelist.error;impact/flowsвозвращают все совпавшие определения, а не выбирают одно. 19 тестов, в т.ч. «два вида не дают одинаковых байт». -
R1-C31 у графа не было одного начала координат для путей ✅ (2026-08-29, issue #12, гэп path_origin_2026-08-29.md, схема 0.12 → 0.13). Репортёр по п. 5
bquearch#5:codemap testsпечатаетpytest tests/test_mod.py::test_f, а файл лежит вresearch/tests/— строка не запускается, но выглядит запускаемой. Напечатанный путь оказался симптомом: каждый корень был своим началом координат, так что приsrc/pkgрядом сtests/в одном графе жили две системы координат, иpkg/mod.pyсtests/test_mod.pyоба читались как репо-относительные, будучи таковым максимум по одному. Пакетный догфуд этого показать не мог — там корни в корне репозитория, и две системы совпадают. Вторая половина: у символов под корнем-потребителем не былоfile(у репортёра 1093 узла против 61),searchотвечал номером строки без файла. Решение: (1) одно начало — ближайший общий предок родителей всех корней;provenance.rootsотносительно него ("src/pkg", а не"pkg"— базовое имя было меньше, чем разрешает D5, и теряло ровно спорный сегмент); (2) где это начало — машинная локация, в граф не идёт, уходит в сайдкар какroots_base(D6: идентичность едет, рецепт остаётся дома); (3)codemap testsразрешает id против сайдкара и печатает путь относительно текущего каталога — только если файл действительно там, иначе оговорка называет каталог; полезная нагрузка op'а остаётся граф-относительной. Из трёх вариантов репортёра взяты (3) целиком, (1) как поведение без сайдкара, (2) в изменённом виде — «как переданы» дало бы абсолютный путь в артефакте. 10 тестов. -
R1-C30-f2
checkмолчал о том, что судил только эагерный граф ✅ (2026-08-29, из замера лабы по п. 3bquearch#5, гэп §12 — закрывает открытый вопрос §7). Репортёр прогнал гейт на дереве с 48 ленивыми циклами и получил✅ Contract satisfied. Rules enforced: no_cycles, тогда какreport architectureна том же графе печаталImport cycles: 0и следомDependency cycles closed only by a function-local import: 48. Его формулировка и есть находка: «checkне упал на неожиданном нарушении — он не упал там, где нарушения есть». Это R1-C29 ровно через день: утверждение о свойстве по частичному виду, только переехавшее из отчёта в гейт — где оно не предложение, а неквалифицированная ✅, которую читатель достраивает до «ациклично». Решение: (1) гейт остаётся эагерным — ленивый импорт это лекарство, ронять сборку за его применение хуже болезни; (2) раскрытие обязательно — проходящийno_cyclesговорит, что судил и что не судил, со счётчиком, и говорит это и при нуле (правило R1-C28: поле, появляющееся только когда есть что сказать, неотличимо от сборки, которая о таком не сообщает); в структурном ответе то же подscope; (3)no_lazy_cycles = true— владелец контракта может занять противоположную позицию («гейт, который обходится ленивым импортом, — не гейт») сам, вместо того чтобы за него выбрали. §7 был прав, что не выбирал дефолт без живого случая, и неправ, что считал «не гейтить» полным ответом: достижимый дефект был не в гейтировании, а в молчании. -
R1-C30-f1 вызов к реэкспортированному имени не резолвился на быстром тире ✅ (2026-08-29, issue #13, гэп §11). Репортёр померил R1-C30 на своём дереве в тот же день и завёл единственный переживший случай:
import registry as _rвнутри функции, шесть вызовов через алиас, пять резолвятся, шестой — реэкспорт — нет. Та же строка, тот же алиас. Воспроизведение показало, что случай шире репорта: fast терял любой вызов к реэкспортированному имени, во всех четырёх формах импорта, включаяfrom pkg.api import runпри реэкспорте из__init__.py— самую обычную форму в Python. Механизм арифметический: резолвер считаетpkg.api.helper, такого определения нет (оноpkg.inner.helper), и охранник R1-C13-f2 честно роняет ребро в никуда. Охранник прав, не хватало поиска — а ответ уже лежал в графе: структурный проход пишетexport-ребро на каждый реэкспорт, и call-резолвер его не читал. Третий раз в этой линии deep выглядел «способнее», хотя просто читал то, что fast не научили читать. Под этим — второй дефект: на плоской раскладкеexport-рёбер не было вообще (R1-C21 научил плоскому правилу пасс импортов, а пасс алиасов — нет), так что до репортёра фикс бы не доехал. Результат: bquantcalls986 → 992 (+6, 6 из 6 подтверждены deep), codemap 502 → 521 (+19, из них 16 подтверждены, 3 — вызовы функций, которые эта же правка и добавила);references+24 и +8; счётчикexportна обеих мишенях не сдвинулся — плоское правило объявляет себя узким. Сьют получилc12_reexportс ловушкой «имя, которого реэкспортёр не несёт»; recall по всем истинным рёбрам fast 64.7% → 66.7%, deep 66.7% → 68.4%. -
R1-C23-f1 закон сохранения ложно срабатывал на repo-scoped графе ✅ (2026-08-25) — поймано в первый же час использования D6:
build codemap --consumer tests --mode fullдал «137 modules built from 48 input file(s)». Причина:inputs.python_filesсчитает обход ядра, а счётчик модулей брал и консьюмер-руты (tests/, включая фикстур-пакеты). Проверка теперь считает только модули сroot == "core"— 48 против 48, молчит. Мораль в пользу самой проверки: она сработала на первом же живом расхождении, просто расхождение было её собственным. -
R1-C25 провенанс графа: артефакт не говорит, кто его собрал, из чего и когда он перестал быть правдой ✅ ✅ (2026-08-25, схема 0.11 → 0.12) — замерено, спроектировано и построено в тот же день. Источник — прожитый эпизод на R1-C22-f1:
test_determinism_with_semanticsпокраснел, и причина была не в недетерминизме — таргет правился соседним процессом между двумя сборками. Ни граф, ни тест, ни тул не могли отличить «инструмент недетерминирован» от «вход сдвинулся»; развели вручную, пересборкой на замороженной копии. Гэп: уgraph.jsonровно четыре ключа верхнего уровня —codemap_schema,target,nodes,edges. Ни версии тула, ни ревизии исходника, ни тира, ниscope_id. Аcodemap_schema— единственное, что выглядит провенансом, — пишется и никогда не читается:Graph.from_dictиstore.loadего игнорируют. Замер: замороженная копияtests/fixtures/refpkg, собранная codemap'ом на4858899и на16fe7de(4 коммита) — 30 рёбер против 38,dead-code high— 12 против 7. Пять функций, которых один граф зовёт мёртвыми, а другой живыми; оба объявляютcodemap_schema: "0.11", и текущий тул грузит любой без единого слова. И заметь, что здесь ничего не нарушено: R1-C22 не добавила ни kind'а, ни типа ребра — толькоextras, которые DESIGN §2 намеренно оставляет открытыми, то есть семантика изменилась правильно, и версия схемы правильно не двинулась. В этом весь довод: провенанс — не схема; схема описывает форму файла, процесс не описывает ничто.codemap diffна этой паре отвечает «✅ No breaking changes» — верно (публичный API не менялся) и потому бесполезно как страховка. Сайдкар не закрывает гэп:*.meta.jsonдействительно несётargv/built_at/cwd/target+scope{scope_id, git, files, profile}, но (1) это отдельный файл, а граф путешествует один — в тикет, в соседний репо, агенту, в контейнер; (2) он в.gitignore, то есть ровно та половина, которой нельзя поделиться; (3) он best-effort: у лежащего сейчас в дереве bquant-сайдкара нет ключаscopeвообще; (4) он пишетcwd— абсолютный личный путь, что по AGENTS.md делает его единственным файлом, который публиковать нельзя. План (дизайнdocs/design/graph_provenance.md, D1–D7): D1 блокprovenanceвнутриgraph.json, схема 0.11 → 0.12 (первая правка с 0.11, которая бампа реально заслуживает — меняется форма), безbuilt_at: детерминизм важнее, часы остаются в сайдкаре; D2versionвсегда +commitкогда резолвится, никогда не выдумывать — обе сборки из замера сделаны версией0.0.2, одной версии мало; хеш исходников экстрактора отвергнут (меняется от правки комментария, кричит волком); D3 проверятьcodemap_schemaпри загрузке — грузим, но громко (старее / новее / нечитаемо → предупреждение на трёх поверхностях: CLI,stats, конвертserve/MCP), отказ отвергнут (все существующие графы 0.11 и старше); D4diffсверяет провенанс первым и предупреждает, когда пара несравнима (разный тул/тир/скоуп), но не отказывает; D5 никаких абсолютных путей в артефакте — тест на это (AGENTS.md); D6 явный раздел: идентичность едет в графе, рецепт пересборки остаётся в сайдкаре (scope_idдублируется намеренно: в графе — идентичность, в сайдкаре — ключ кэша для--incremental). Приёмка: байт-идентичность двух сборок замороженного дерева включаяprovenance(инвариант, которым эта правка рискует больше всего — тест первым); эксперимент из §2 гэпа инвертируется (старый граф грузится с видимым предупреждением,diffназывает пару несравнимой); ноль абсолютных путей;commitотсутствует, а не выдуман, при установке из wheel; все 0.11-графы грузятся. Разблокирует: R1-C23 D2 и D6 (обоим нужен носитель) — поэтому строить первым из трёх; и пост блога P4 (детерминизм), который намеренно ждал настоящего эпизода (у ненаписанного role-provenance-поста «провенанс» — про другое: роли мульти-рута, это соседство R1-C24; зовём его по имени, а не по номеру — номер сдвигается каждый раз, когда вперёд выходит пост с настоящим эпизодом). Приёмка выполнена: эксперимент из §2 инвертирован — граф с4858899теперь рендерится под «graph declares schema 0.11, this codemap writes 0.12 — the artifact predates this tool», аdiffна той же паре ведёт с «the pair cannot be verified» до своего «✅ No breaking changes»; граф bquant байт-идентичен старому, за вычетом нового блока (2833 узла / 8123 ребра без изменений), две подряд CLI-сборки bquant байт-идентичны включаяprovenance; ноль абсолютных путей (проверяется тестом,build_provenanceбросаетValueError);commitотсутствует, а не выдуман, вне git-чекаута; все 0.11-графы грузятся. +28 тестов, сьют 425→453. Чего дизайн не предвидел (и что нашлось только потому, что блок сделал вопрос задаваемым): уbuild --incrementalта же слепота этажом ниже — он решал по одному дереву исходников: ни один.pyне менялся → вернуть старый граф. После апгрейда codemap это возвращает вчерашний граф, собранный вчерашним экстрактором, и рапортуетmode: unchanged.update_graphтеперь сверяет tool-идентичность и тир и падает в полную пересборку (reason: "builder-changed"); граф без провенанса считается другим сборщиком — консервативное направление (лишняя полная пересборка стоит минуту, молча устаревший граф — неверный ответ). Живая иллюстрация: между первой и последней сборкой этой вехи записанныйsource.commitподопытного репо сам сдвинулся6bbb142→f8765cc— соседний агент коммитил в bquant по ходу; ровно то состояние, из-за которого 2026-08-24 покраснел тест детерминизма, теперь поле в артефакте, а не час ручной разборки. Урок применён к тестам, с которых всё началось: четыреtest_determinism_*собирали живой чекаут bquant дважды и сравнивали байты — это меряет не детерминизм codemap, а «не коммитил ли кто-то последние пятнадцать секунд»; один из них покраснел прямо по ходу этой вехи (соседний агент положилf8765cc). Теперь они собирают замороженный снимок (tests/frozen.py) — та же дисциплина, что и на уровне артефакта: заморозь вход, иначе меряешь не то. Отдельно проверено на замороженной копии bquant: два extract'а байт-идентичны, 3.9 МБ. Пользовательские доки:docs/provenance.md. Разблокировало: R1-C23 D2 и D6 (носитель есть). Гэп:gaps/graph_provenance_2026-08-25.md.
- R1-C9 Инкрементальные обновления графа ✅ (2026-08-23, без схемы) — строится на M19.A
(per-file sha256 манифест) + M18 (sidecar
.meta.jsonсо scope). Ключевой замер: полный deep-экстракт bquant ~97s, из них ~93s — jedi-тир (add_behavior(deep)+add_attrflow(deep)); всё остальное (griffe + структура + dispatch + family + dataflow) ~4s. Значит инкремент имеет смысл только для deep, и единственная дорогая помодульная работа — два jedi-паса. Сделано:codemap/incremental.pyupdate_graph(old, pkg, old_scope, new_scope, deep)— дешёвые пасы гоняются целиком и свежими каждый раз (всегда корректно), два jedi-паса — только на affected-модулях (only=-параметр вadd_behavior/add_attrflow), остальное сплайсится из старого графа (calls/accesses-рёбра + extras calls/control/complexity/attr_access). Affected = changed/added/removed ∪ свежие импортёры изменённого ∪ старые зависимые (old-рёбра в изменённое). Fallback на полный ре-билд при >50% задетых. CLIbuild --incremental(переиспользует--out-граф + scope-sidecar).to_dictтеперь сортирует рёбра и поextras→ порядок insertion-независим (сплайс сериализуется как полный билд). Приёмка: правка одного файла → incremental deep ~5s vs full ~60s (~12×), только 2 модуля пересчитаны; на fast-тире байт-в-байт идентично полному (bquant 7437 рёбер, 0 расхождений), +12 тестов (byte-identity на edit/add/remove/оба тира + no-op + fallback). Открытие (важное, честное): deep-тир не детерминирован — два полных deep-билда bquant сами отличаются на пару deep-рёбер (jedi bounded inference зависит от прогретости кэша:resolved 11↔12). Значит «идентично полному» — строго только на fast-тире; на deep инкремент идентичен полному с точностью до собственной вариативности jedi (её же имеют и полные билды), сам сплайс точен (доказано byte-identity на стабильной фикстуре). Задокументировано вdocs/incremental.md. Питает отложенный M3.2 watcher (hot-pathmode=unchanged). - R1-C38 тёплый сервер держит и граф, и код — перезагружается только граф ✅ (XS) —
(2026-08-31, измерено при закрытии долга, а не придумано).
Закрывал пункт «MCP-путь для новых полей гонял через
Session.handle, не через живого агента»: пересобрал граф bquant версией 0.0.5, вызвалreload— подхватился (4656 → 4743 узла, диагностика о расхождении схем ушла), а досье вернулось безsignature, который в пересобранном артефакте лежит. Ничего не сломано: процесс сервера был запущен до R1-C33 и держал старыйbuild_query_resultнад новым графом. В ответе не было ни слова об этом, а следующий ход пользователя — сноваreload, который здесь не поможет никогда. Сделано:tool_drift(running, installed)— сравнение версии, с которой загружен код (codemap.__version__, зафиксирована на импорте), и версии, которую сейчас сообщает установленный дистрибутив (tool_version()читает метаданные в момент вызова). Расходятся — диагностикаtool_restart_neededвstatsи вreload: операция, которая обещает свежесть именем, обязана назвать ту половину, которую не обновляет. Обе стороны расхождения сообщаются (процесс новее установленного — такое же несовпадение); неизвестная версия — молчание, а не выдуманное предупреждение. 6 тестов. Родословная: это R1-C25 («две разные вещи делили одно поле») на уровень выше — там разъехались схема графа и схема тула, здесь код процесса и установленный код. - R1-C38-f1 предупреждение о рестарте не могло сработать никогда ✅ (XS) —
(2026-09-01, найдено верификацией релиза: прогон опубликованного колеса через сам сценарий —
поднять сервер, сменить установку под ним, спросить
stats).Session._tool_driftчитал установленную сторону черезtool_version(), а он@lru_cache(maxsize=1)и первым вызовом обслуживает импортcodemap/__init__.py— тот самый, что задаётcodemap.__version__. Обе стороны сравнения — одно и то же закэшированное значение, в любом процессе и навсегда:tool_restart_neededбыл недостижимым кодом при зелёном сьюте. Почему сьют молчал: шесть тестов давалиtool_drift()две версии аргументами, седьмой патчилcodemap.__version__, чтобы расхождение возникло. Все они проверяют арифметику; ни один не спрашивает может ли условие возникнуть. Это ровно тезис 0.0.6 («проверка, которой ни разу не показали то, что она обязана отвергнуть, — не проверка»), и релиз с этим тезисом отгрузил его нарушение. Сделано:provenance.installed_version()— некэшированное чтение метаданных на каждый вызов (замерено: апгрейд во время работы процесса виден свежемуimportlib.metadata.version(), безinvalidate_caches());tool_version()остаётся кэшированным — сборка обязана штамповать одну версию во всё, что пишет. Два читателя существуют потому, что отвечают на разные вопросы: «какая была?» и «всё ещё та?». Регрессионный тест не патчит процесс — подкладываетdist-infoраньше поsys.path, как это делает апгрейд; на проводке 0.0.6 краснеет. 2 теста (8 в файле). - R1-C36-f2 на плоской раскладке дефект был хуже: не подмена, а объединение ✅ (XS) —
(2026-08-31, ответ на вопрос, который я лабе обещал и сказал, что дать не могу).
Сказал им: «на плоской раскладке не воспроизводил, там модули-соседи, а не пакет». Оказалось,
воспроизводимо без их кода — синтетическая namespace-раскладка достаточна.
Замерено на выпущенном 0.0.4: при одноимённом каталоге в рабочей директории граф пришёл
объединением обоих деревьев —
core.registryиcore.userиз цели иcore.impostorиз затеняющего. Namespace-пакеты аддитивны: griffe не выбрал одну часть, а слил найденные, и результат описывал код, которого не существует нигде. Заметить это труднее, чем пакетный случай: число узлов растёт, а не сдвигается, и каждый символ в графе — настоящий символ из настоящего файла. Ровно то, чего сама подмена не давала: там граф был чужим целиком, здесь он частично свой. На 0.0.5 случай закрыт тем же фиксом (try_relative_path=Falseоставляет одну часть), и гвард R1-C36-f1 при этом не ломает законное объединение — цель среди частей проходит. 1 тест. - R1-C36-f1 гвард цели на составном namespace-пакете ✅ (XS) — (2026-08-31, долг, названный
вслух в записке лабе: «гвард проверен на обычном пакете и на namespace-каталоге; на пакете,
разложенном по нескольким
search_paths, — нет»). Пакет, собранный из нескольких частей, — законный ответ: запрошенный каталог просто находится среди них. Сквозной путь до этого случая не достаёт, потому что сборка передаёт ровно одну записьsearch_paths, — то есть гвард, отвергающий такую форму, не был бы пойман ничем. Поэтому решение вынесено в_assert_is_the_target()и проверяется напрямую: список из двух частей, среди которых цель, — проходит (в обоих порядках); список, где цели нет, — падает и называет обе стороны; одиночный файл сравнивается по своему каталогу; пустой ответ — не несовпадение («griffe промолчал о файле» ≠ «griffe назвал не тот файл»; выдумать отказ из молчания — та же ошибка, только в другую сторону). 4 теста. - R1-C37 мутационная проверка всех шести правил контракта ✅ (S) — (2026-08-31, метод лабы,
применённый к себе).
Лаба не поверила зелёному гейту
no_lazy_cyclesи вернула одно ленивое ребро, чтобы посмотреть, покраснеет ли. После повторения этого на двух своих деревьях очевидный вопрос: кто из шести правил когда-либо получал такое обращение. Уточнение к собственной оценке: я сперва сказал «ни одно», это было слишком сильно. Тест на нарушение есть у каждого правила (test_r1c3_arch_contract.py), и каждый построен на рукотворном графе из трёх узлов, где слои вписаны в id, аrootпроставлен руками. Это проверяет арифметику правила. Чего оно не проверяет — что правило находит настоящее нарушение в настоящем графе: между исходником и правилом не участвует ничего — ни экстракция, ни вывод слоя из дерева пакетов, ни реэкспорты, ни ленивые рёбра, ни тысячи законных импортов, среди которых нарушителю надо потеряться. А догфуд-тест гоняет реальное дерево и утверждает зелёное — ровно та форма свидетельства, от которой лаба отказалась. Сделано:tests/test_r1c37_rule_mutation.py— одна копия собственного пакета, и на каждое правило: зелено → одна строка правки → красное с тем самым ребром → откат → файл побайтово прежний. Контракт в каждом случае содержит только проверяемое правило, чтобы ничто другое не могло сработать и быть принятым за него (мутация дляlayersзаодно создаёт цикл — под layers-only контрактом их не спутать). Плюс пара, ради которой правила разные: на ленивой мутацииno_cyclesмолчит, аno_lazy_cyclesкраснеет — на реальном дереве, а не на фикстуре. Побочно найдено:Violation.modulesу циклов несёт отрендеренные пути («a → b → a»), а не идентификаторы модулей; тест на это чуть не был написан неверно, теперь форма зафиксирована. И:codemap.tomlобъявляет десять слоёв из шестнадцати реальных компонентов — для чтения это правильно (необъявленный слой инертен), но дляexhaustiveбазовая линия обязана называть все, иначе она красная ещё до мутации. Цена: ~22 с, семь экстракций 90-файлового пакета. 8 тестов. - R1-C36 сборка молча анализировала не тот пакет ✅ (S) — (2026-08-31, найдено при проверке
правила
param-made-keyword-onlyна настоящей паре релизов). Разложил тегиv0.0.3иv0.0.4в скретч, собрал оба, и графы вышли с одинаковым набором узлов и одинаковым числом рёбер — 2973 → 2973 при девяти различающихся файлах исходников. Ни один из двух не был графом своего тега: оба описывали рабочее дерево, потому что сборка запускалась из корня репозитория. Механизм.griffe.load(name, ...)по умолчанию идёт сtry_relative_path=True— трактует имя модуля как путь относительно текущего каталога, и это перебивает переданныйsearch_paths. То естьcodemap build /elsewhere/pkg, запущенный из репо, у которого в корне лежитpkg/, анализировал локальныйpkg. Выход 0, ни слова предупреждения, полный well-formed граф не того кода. Почему не замечали. В обычном случае оба совпадают: свой пакет из своего корня. Расходятся они ровно там, где разница и нужна — архив тега, worktree, копия, — то есть в любом двухснимочном сценарии, ради которого существуетcodemap diff. Релизный гейт, сравнивающий два таких графа, отвечает «изменений API нет» всегда, и чем аккуратнее гейт, тем тише ложь. Абсолютные пути вfileбыли симптомом того же: загруженные файлы лежали не под переданным корнем, релятивизация тихо сдавалась, и артефакт нарушал D5. Сделано: (1) имя резолвится только черезsearch_paths(try_relative_path=False); (2) после загрузки проверяем, что разрешённый путь лежит под запрошенным каталогом, иначе — громкая ошибка с обоими путями: что бы ни сделали дальше.pth, namespace-пакет или новый дефолт griffe, несовпадение здесь ниже по течению не видно, поэтому молчать ему нельзя. Гвард сразу же поймал собственную ловушку: у namespace-пакетаfilepath— список (форма из issue #4), первая версия проверки на нём падала; сьют это показал до коммита. Проверено на том, что и нашло: пересобранная пара тегов даёт 934 → 1022 узла, 2674 → 2960 рёбер,diffотвечает «28 added, 0 removed, 3 changed, no breaking» —limit: int → int | Noneкак warning, два опциональных параметра как info. 4 теста. - R1-C35
checkне говорил, где искал контракт ✅ (XS) — (2026-08-31, из лабы, bquearch#5). Прислано как потерянная минута, а не как баг: человек потянулся заcheck --config codemap.toml(такого флага нет), получил «No[architecture]contract found in codemap.toml — nothing to enforce» и не смог отличить «контракта нет» от «я стою не в том каталоге». Оба ответа — выход 0. Их же формулировка объясняет, почему это наша линия, а не только доки: «дефолт (отсутствие контракта = тихий успех) считаю правильным, но именно поэтому--require-contractстоит упоминать рядом с примером: без него легко получить зелёное на пустом месте — тот же класс, из-за которого началась вся линия». Решение — не в коде выхода. Дефолт правильный: проект без контракта падать не должен. Неправильно то, что единственный прогон, который никто не читает, — зелёный, а этот конкретный зелёный означает «не проверено ничего». Сделано:ArchitectureContract.path(абсолютный — «не найдено в codemap.toml» читатель и так предполагал, вся ценность строки в том, в каком именно); сообщение при промахе называет путь, говорит вслух «и это выход 0» и называет--require-contractи--root;contract_pathв JSON-поверхности (машинный читательcontract_emptyзадаётся тем же вопросом). Артефакт графа абсолютных путей по-прежнему не несёт (D5) — это вывод в терминал, не артефакт. Доки: правило поиска и «флага--configнет» перенесены к первому примеру, а не через сорок строк после него;--require-contractпомечен как то, что ставят в CI. 6 тестов. - R1-C34 сигнатура теряла вид параметра ✅ (S) — (2026-08-31, найдено при работе над R1-C33).
Первый же тест на досье потребовал сигнатуру
run(target: str, *, retries: int = 3) -> bool, а рендер отдалrun(target: str, retries: int = 3) -> bool. Дефект стоял с M0 и был двойным: маркеры*и/пропадали (keyword-only читался как позиционный — напиши вызов, который описывает сигнатура, и получишьTypeError), а*args/**kwприезжали обычными параметрами с выдуманным дефолтом из griffe (args = (),kw = {}), которого в исходнике нет:def h(*args, **kw)(принимает что угодно) иdef h(args=(), kw={})(два опциональных позиционных) рендерились в одну строку. Замерено на двух деревьях: codemap — 62 из 435 функций (14.3%; 47 keyword-only, 6 вариадик), bquant — 152 из 931 (16.3%; 140 вариадик). Среди пострадавших наша собственная публичнаяresolve_scope: шесть keyword-only параметров документировались как позиционные. Почему это не косметика. Строку читают пять потребителей:report api-surface,export ctags,export vault, living-docs и (с R1-C33) досьеquery, аapidiffеё переразбирает какdef <sig>: .... Там потеря вида тихо выключила три собственных правила:has_varargиhas_kwargне могли стать истинными никогда (правило «*argsудалён» — недостижимый код), а_Param.keyword_onlyпарсился с самого начала и не читался ни разу. Итог:def f(a, b)→def f(a, *, b), который ломает каждого позиционного вызывающего, классифицировался как отсутствие изменений. Сделано: рендер держит вид (/,*,*args,**kw, PEP8-пробелы) + правилоparam-made-keyword-only(breaking) и обратноеparam-made-positional(info, расширение) вapidiff. Тесты (37): по одному на форму, round-trip «отрендерили → перепарсили → виды совпали с исходником» (это и есть свойство, строки — лишь его написание), и парсимостьdef <sig>: ...для каждой формы. Схему не бампаем: форма графа не менялась, менялось содержимое поля, смысл которого («как объявлено») прежний. Но старые графы пересобрать: диф старого снимка против нового покажет расхождение рендера, а не кода — записано вdocs/api-diff.md. - R1-C33 сигнатура в досье
query✅ (S) — (2026-08-31, при аудите урожая из разбора CodeGraph). Карточкаresearch/tools/codegraph.mdзаписала это 2026-08-28 вторым пунктом «что берём» — и пункт никем не был заведён: три из четырёх пунктов того списка закрыты или отмечены как предусловие, этот пролежал три дня как невидимая строчка внутри прозы. Сам факт — иллюстрация к тому же правилу, что и R1-C28: список без статуса читается как выполненный. Что не так по существу:build_query_resultотдаётmatchesкак{id, kind, file, lines}. Агент, узнавший имя, следующим вопросом хочет сигнатуру — и обязан сделать второй вызов (call_contract), хотяNode.signatureуже лежит в графе иserve/api_surface.pyего уже читает. У пира сигнатура приходит инлайн первым же ответом, и это единственное место, где их досье удобнее. Scope:signatureв элементахmatchesдляfunction/class(иdeprecated, раз рядом); ничего не считаем заново — читаем узел. Не путать сcall_contract: тот отвечает, как зовут по факту (posargs/kwargs/splat по каждому call-site); этот — как объявлено. Приёмка: досье функции несётsignature; узел без сигнатуры (модуль, атрибут) поля не получает, а не получаетnull; тест на обе формы + паритиcodemap queryи warm-serve (один билдер). Сделано:_match()вserve/session.py— один билдер на CLI, warm-serve и MCP. Функция несётsignature; класс несётconstructor(свой__init__) под собственным именем, а не переодетым в «сигнатуру класса», которой у класса нет; унаследованный__init__не разрешается — по базам не ходим, и молчание тут честнее догадки (unknown ≠ none). Рядомdeprecated: true, раз лежит на том же узле. Ни одно поле не приезжает какnull: сказать нечего — ключа нет, иначе «не смотрели» неотличимо от «нет». Текстовая форма печатает сигнатуру строкой ниже локации, конструктор — с меткойconstructor:. 11 тестов, включая парити CLI ↔ warm-serve. Описание MCP-тула разводит два вопроса явно:signature— как объявлено,call_contract— как зовут по факту. Побочный улов: первый же тест уронил рендер сигнатур — см. R1-C34 (закрыт тем же днём, до этой карточки). - R1-C10 lightweight навигатор графа (L) — ниша ушедшего Sourcetrail без GUI-налога.
Scope: статический self-contained HTML/mermaid-навигатор поверх
graph.json(клик по символу → соседи/impact), генеритсяcodemap export --view web. Зачем: Sourcetrail умер на поддержке кросс-платформенного GUI; наш детерминированный граф закрывает нишу дёшево. Приёмка: один HTML открывается без сервера, навигация по bquant-графу работает офлайн. Оценка: L. Четыре чужих проектных решения, если дверь откроется (2026-09-16, интейк R1.6research/06_multimodal_repo_views.md§6): (1) вид как функция фазы агента (localize / edit / test), а не только запроса — у насpackзависит лишь от вопроса; (2) две согласованные проекции одного отбора — у нас обе половины уже есть и никогда не были связаны (pack= текст,export mermaid --scope= картинка), не хватает «grounding references»: элемент картинки несёт точный символ и локацию; (3) бюджет обязан быть в ответе (R1-C28): у них он фиксированный и крошечный — 15 узлов, 20 рёбер, — и по картинке этого не увидеть; (4) раскладка по топологии (force-directed / иерархическая / flowchart): у нашего mermaid-экспорта одна, а на плотном клубке (19 модулей_pytest) одной не хватает, чтобы вид был читаем. - R1-C11 tree-sitter multi-language backend (XL) — выход за Python. Scope: tree-sitter (доказано ast-grep) как source-only/детерминированный/offline бэкенд ширины; глубина (call-graph/impact/ contracts) остаётся за jedi/griffe — это ров codemap. Зачем: мультиязычность. Приёмка: структура (defs/imports) для ≥1 не-Python языка в том же графе. Оценка: XL. Смыкается с «Мультиязычность» ниже; брать только при явной нужде.
- R1-C12 rope-безопасные правки (L) — опциональный слой мутаций (rename по вычисленному blast-radius). Нарушает read-only-дефолт — держать за отдельным флагом/extra; read-only остаётся дефолтом. Зачем: от анализа к безопасным правкам. Приёмка: rename символа по impact-радиусу, dry-run по умолчанию. Оценка: L. Только если codemap пойдёт к правкам.
- R1-C13 Бенчмарк call-graph + grep-vs-graph + честный потолок ✅ (2026-08-14, без схемы). Приёмка
закрыта:
docs/accuracy.md(раздел) + два бенч-скрипта + CI-тесты (tests/test_r1c13_*.py). (a) точность/потолок: PyCG-как-оракул — спайк-негатив (research/tools/pycg.md): PyCG 0.0.8 не запускается на Python 3.12 (import-hook хачит stdlib, падает даже на 3-строчном файле — три слоя поломки, третий структурный; та же «хюбрис-зона», что и graphlens). Пивот: свой ручной ground-truth микро-сьют (research/bench/callgraph_truth/, 10 кейсов: direct/self/ cross-module/higher-order/decorator/inheritance/getattr/local-var/registry/closure) → deep-tier precision 100% / recall(decidable) 100% / recall(all) 60% (callgraph_accuracy.py). Литературный потолок PyCG (~99% precision / ~70% recall, ICSE 2021) процитирован, не запускался. Intrinsic-резолв на bquant@cb89a24: 6323 call-sites → 25.7% resolved / 46.1% external / 28.2% unresolved. (b) grep-vs-graph:grep_vs_graph.py(авто-таргеты, без черри-пика) на bquant: BREAKAGE — граф дешевле grep в ~11× (unique) → ~38× (polymorphic, до 55× наcalculate); WHERE-DEFINED — ~1× (нет выигрыша, grepdef NAMEуже точен) — честный нуль удержан. Дифференциатор: ценность графа на связях, не локациях. Дожфуд-CI на самом codemap (fast-tier, <1с). - R1-C13-f1 (soundness) fast-tier наследование → фантомный таргет ✅ (2026-08-14, без схемы) — из
микро-сьюта (c06).
_class_scope→_class_membersтеперь маппит каждый унаследованный член на класс-владелец (свой или базовый), иself.<inherited>()резолвится в реальный id базового метода, а не в фантомThisClass.<inherited>. fast-precision 87.5% → 100%. - R1-C13-f2 (soundness) call в замыкание → ребро в неузел ✅ (2026-08-14, без схемы) — из
микро-сьюта (c10). Общий guard в
_process_function: внутреннее call-ребро с таргетом-неузлом (локаль, которую jedi типизировал в её же scope-path; вложенная/closure-функция) даунгрейдится в unresolved, а не эмитится в никуда. Побочно убрало 40 латентных фантомов на bquant-графе (callers/callees/impact больше не всплывают id, которых нет как узла). Материализация closure-узлов — будущая работа. - R1-C14 Позиционные доки ✅ (2026-08-06) — заведён
research/positioning.md— публикационный слой (build-story hub): Story Zero (codemap + роадмэп M0→M19, дифференциаторы, честные дыры) + полная Build-story #1 (graphlens) с цифрами и «эмоцией» по горячим следам. Тезис: «codemap = точная структурная нога для index-free агентов через MCP» (не замена embeddings-RAG); дифференциаторы (каноничный diffable граф + provenance + agent/MCP-глаголы; MIT против non-commercial у GitNexus; SCIP-interop; honesty). Закрыто: корневойREADME.mdполучил секцию How it compares (тезис + позиционная строка + линки на positioning.md/comparison.md). Наполнять build-story по мере разборов (это continuous — живёт в positioning.md, не блокирует R1-C14). R1.5 добавил остроты: ниша переполнена → ров заявлять громко. - R1-C15 Living docs из графа ✅ (2026-08-07, без схемы).
codemap export docs— нарративный документ, организованный по подсистемам (communities R1-C18), а не плоско по модулям.serve/livingdocs.pyrender_docs. Honesty-контракт = дифференциатор (vs CodeWiki/нейростатьи, которые галлюцинируют): всё трассируемо — структура (модули/классы/функции/imports/inheritance) = точный факт; докстринги цитируются verbatim (слова авторов, не генерятся); недокументированный символ помечается (_(undocumented)_), не выдумывается; call-flow-секции несутepistemic: partial(нижняя граница). Секции: overview+counts → subsystems (с публичными символами) → ungrouped-модули (полнота) → behavioural entry points (flows) → architecture-заметки (циклы/violations/god-objects) → honesty-footer. CLIexport docs [-o]+ serve export-viewdocs. Core-only (не документируем tests/docs-роуты). Побочный фикс:Query.communities()теперь кластеризует только core-модули (consumer-роуты утягивали ярлык в «(root)») — улучшило и R1-C18: на bquant ярлыки стали реальными (data/analysis/ indicators вместо «(root) 45»). Проверено на bquant: 165 модулей, подсистемы, docstrings verbatim, god-objects, footer. +11 тестов. Полный прогон 220 passed/1 skip. Питается communities+flows+epistemic. - R1-C16 Роутер/адаптер-слой над внешними тулами ✅ (2026-08-16, без схемы графа). Обе половины
готовы и разведены. Router-путь был (GitNexus passthrough,
codemap route); достроена adapter- половина: базовый контрактSemanticHit+ retrieval-поверхностьIntegration.search()(integrations/base.py); cocoindex-адаптер (integrations/cocoindex.py, Apache-2.0, capabilitysemantic-search— зовётccc search --jsonв cwd репо, отдаёт сырые хиты); энричерintegrations/semantic.py— резолвит ADAPTER (не router), мапит каждый fuzzy-хит(file,line)→ точный символ codemap черезQuery.symbol_at, дедуп по символу (лучший score), сортировка.registry.resolve(mode=ADAPTER)— фильтр по режиму (semantic берёт именно адаптер, не роутер). Поверхности: CLIcodemap semantic "<query>"(--build/--graph, --root, --limit, --format) + serve-opsemantic+ MCP-toolsemantic_search. Лицензионная политика машинно-проверяется (adapter не- permissive → ValueError). Ядро работает без тула (нет adapter →{resolver:None, hits:[]}, не падение). Приёмка выполнена: detect (which ccc) ✅, opt-in (codemap.toml) ✅, оговорка (router-путь) ✅, перевод в символы (adapter) ✅, degrade ✅. +12 тестов (FakeAdapter — обобщённый путь; monkeypatch cocoindex argv/parse; live-skip если нет ccc). Докиdocs/integrations.md. Обобщено под будущие адаптеры (любой permissive retrieval-тул регистрируется так же). Оригинальная карточка ниже 👇 Исходная формулировка (2026-08-06, из разбора GitNexus) — архив, не открытая работа: codemap не только самописный тул, но и каркас-роутер над сторонними: для способностей, которых у нас нет и строить свои нет смысла (семантический поиск, много языков, flow/community-нарратив), вызывать чужой тул за адаптером и переводить результат в наш нейтральный граф-контракт (реализует DESIGN §13). Два режима: адаптер (переводим ответ в нашу схему — только для лицензионно-совместимых, MIT/Apache) и роутер (перенаправляем вопрос, отдаём ответ как есть — для тяжёлых/чужих, opt-in subprocess). Лицензионная политика интеграций (решено с пользователем 2026-08-06): (1) никогда не бандлить внешний тул — только вызов установленного пользователем (codemap ничего не распространяет → ядро остаётся чистым MIT); (2) opt-in, не по умолчанию; (3) оговорка по критерию «использование», не «перепродажа» — для non-commercial-тулов (PolyForm-NC и т.п.) показывать разовую оговорку: «тул X под лицензией PolyForm Noncommercial; этот маршрут — только для некоммерческого использования; при коммерческом не задействуйте его / возьмите enterprise-лицензию у автора»; (4) при открытии codemap — спросить автора тула про явное разрешение на роутинг. Приватная фаза: ограничений нет. Первый капабилити-кандидат ПОДТВЕРЖДЁН разбором (2026-08-16): семантический поиск через cocoindex-code (ccc) — Apache-2.0, локальный (без БД/ключа), CLI+MCP, hands-on-измерен (research/tools/cocoindex-code.md); лицензионно-чистый → пригоден к adapter (перевод в наш контракт), не толькоroute. GitNexus остаётсяlearn+ NC-routable-plugin (не adapter). Приёмка:codemapумеет продетектить установленный внешний тул, сроутить в него opt-in запрос, показать лицензионную оговорку, перевести/ прокинуть ответ; ядро работает и без него. Оценка: L. Связь: DESIGN §13, [research/tools/gitnexus.md], реестр кандидатов (§13.1). - R1-C16-f1 (router)
npx-фоллбэк детекта GitNexus ✅ (2026-08-15, без схемы) — из живого вопроса «почемуwhich gitnexusпусто, мы же ставили». Причина: GitNexus ставится как локальный npm-пакет (npm install gitnexus, 1.7 ГБnode_modules→ бин в./node_modules/.bin/, запуск черезnpx), глобальногоgitnexusна PATH не было никогда; нашis_available()дергал толькоwhich("gitnexus")→ всегда «unavailable». Фикс:GitNexusRouter._launcher()— глобальный бин, если на PATH; иначеnpx --no-install gitnexus(работает с локальной инсталляцией, без скрытой сетевой доустановки — при отсутствии деградирует в None, не качает); иначе None.is_available()/route()идут через launcher, argv собирается launcher-независимо (_verb_argv). Снимает «setup friction — high» из карточки: роутер живёт безnpm install -g. +5 тестов. Связь: [research/tools/gitnexus.md], DESIGN §13.1.
Прогон обоих проработанных тулов через рамку решения (DESIGN §13.1). Вывод: почти всё уникальное строится на своём графе (networkx + канон уже есть) и лучше ложится на тезис детерминизм/provenance; настоящий адаптер один.
- [~] R1-C17 graphlens-адаптер: резолв внутрь зависимостей — 🔬 спайк сделан (2026-08-07) → ОТЛОЖЕНО (негатив).
Гипотеза: codemap source-only-of-target (не резолвит в библиотеки), graphlens (MIT,
ty-бэкенд) умеет — обернуть адаптером,calls_external→external_symbolв сайдкар. Спайк-first проверил на синтетике (mypkg.core.dumpзовётjson.dumps; контроль —caller→helper): graphlens действительно пишет cross-boundary-рёбра (calls: dump → external_symbol,resolves_to: import json → external_symbol {origin: stdlib},has_typeна внешние типы) — этого у codemap нет. НО два блокера убивают ценность сейчас: (1) внешний член не именуется — цель вызова = span-плейсхолдерcall@6:17, а неjson.dumps; graphlens знает «вызов уходит в stdlib здесь», но не какой символ → заявленная ценность «какой pandas-API зовёт код» не отдаётся на symbol-гранулярности; (2) реальный third-party кейс ломает резолвер — на pandasty serverтаймаутил (1s/30s повторно), 0 узлов за 10+ мин; собрался только stdlib-only проект, эмбеддинги упали (нет egress). Вывод: абсорбировать недетерминированные, низко- разрешённые, хрупкие данные в сайдкар ради маржинального выигрыша над уже имеющимися external-leaf-узлами codemap — не стоит. Пересмотреть если: graphlens начнёт именовать внешний член (не span) и/или ty стабилизируется на тяжёлых deps. Спайк-first гейт сработал — сэкономил постройку P3. Связь: DESIGN §13.1, [research/tools/graphlens.md]. - R1-C18 Communities + flows (на своём графе) ✅ (2026-08-07, без схемы). Построено на своём графе +
networkx (подсмотрено у GitNexus, но считаем сами — детерминированно). (a)
Query.communities()— кластеры модулей через greedy_modularity_communities (детерминизм by construction, on-brand vs seed-Louvain) над undirected import-графом, ярлык = доминирующий слой; (b)Query.entry_points()— корни call-леса (зовут, но не зовомы) по роуту; (c)Query.flow(entry, max_depth)— форвардный call-flow поcalls(зеркало impact), рёбра с distance, cycle-safe. Рендерserve/subsystems.py(render_communities/render_flows); CLIreport communities+report flows [--symbol X] [--depth N]; serve-opscommunities/flows; MCP-toolscommunities/flows(18→20). Проверено на bquant: 16 кластеров подсистем; 224 entry points, ранжированы по reach (swingcalculate→ 32). +11 тестов (конструированные 2-кластера + call-chain; CLI; serve). Полный прогон 204 passed/1 skip. Питает R1-C15 (living docs). Строим сами (не wrap). - R1-C19 Транзитивный impact + depth-гистограмма + risk ✅ (2026-08-07, без схемы). Транзитивный BFS уже
был (M6.6,
impact(depth=2)метитdistance); добавлено то, чего не было и что подсмотрено у GitNexus, но построено на своём графе: (1)by_distance-гистограмма +max_distanceвQuery.impact; (2) эвристичныйrisk(none/low/medium/high) из формы blast-radius — breadth × reach × root-spread (последнее — наш провенанс-дифференциатор: символ, задетый в core+tests+docs, дороже менять); (3)--depthна CLIreport impact+ проброс в serve report-op; (4) рендер показывает Risk + гистограмму. MCP-compact сохраняет новые поля (они на entry, не на refs). Проверено на bquant:MACDZoneAnalyzer→ 28 refs (core 2/docs 7/ tests 19), Risk MEDIUM (3 роута), d1×28. +10 тестов (конструированный граф — точный контроль; CLI; serve). Полный прогон 193 passed/1 skip. Строим сами (не wrap). Смыкается с M6 (impact) и R1-C5 (diff). - R1-C13 (расширение) epistemic-метка ✅ (2026-08-07, без схемы). Вариант 1 (одна метка на ответ, без
per-edge confidence — рёбра уже несут
resolution). Serve-конверт для call-graph-зависимых ops (callers/callees/impact/flows/call_contract) несётepistemic: "partial"+ причина — машиночитаемый двойник прозаических дисклеймеров; структурные ops (imports/contains/…) метку не несут (отсутствие = exact). Переживает MCP-компакцию. +5 тестов. Полный прогон 209 passed/1 skip. Остаток R1-C13 (бенчмарк PyCG + grep-vs-graph) — отдельно, см. основной R1-C13 выше.
Дизайн: docs/design/scope.md (на ревью, до кода). Мотив: codemap
детерминирован на выходе (graph.json), но про вход фиксирует только команду сборки (M18-мета);
конкретный набор файлов лишь приблизительно восстановим, без контент-хэшей и профиля. Нужно для (1)
воспроизводимости/трекинга изменений codemap и (2) валидных сравнений R2 (доказать, что тулы видели
байт-идентичный вход — пилот graphlens показал, как venv_bquant тихо меняет скоуп).
Модель (общая): spec (декларативный набор: roots+role, include/exclude с дефолтами против venv_*/кэшей)
→ резолв в отсортированный список файлов → манифест {path, sha256, bytes} + профиль (файлы/байты/
by_role/by_ext/py_loc) → scope_id (sha256 по отсортированным path\tsha256). Одинаковый scope_id ⇒
доказуемо одинаковый вход.
- M19.A codemap-фича: манифест входа ✅ (2026-08-02) —
codemap/scope.py(резолв+хэш+профиль+scope_id); git-биндинг (O6 ✅): перечисление черезgit ls-filesв git-репо (gitignore-корректный набор — venv/build/кэши исключены сами) + fs-fallback; блокgit {commit,ref,dirty,dirty_files}и бесплатныеgit_blobв манифесте; идентичность = наш sha256 (mode-independent). CLIcodemap scope <path> [--consumer/--docs/--spec] [--no-git] [--json](резолв без сборки) иcodemap scope --diff <a.meta> <b.meta>(делегируетgit diff, когда обе стороны git-clean);buildпишет блокscopeв M18-сайдкар.graph.jsonостаётся чисто структурным (без схемы). Тесты: детерминизм, флип одного хэша,--diff, excludes, git-mode vs fs-mode, dirty. Субстрат для отложенного: контент-хэши = вход Merkle для R1-C9 и hash-свежести для M3.2. Три пункта сходятся сюда. - R2.0.1 харнесс бенч-скоупа ✅ (2026-08-03) — см. ниже, в R2 (реализован поверх M19.A).
Решения — все приняты (2026-08-02): O1 scope_id только в сайдкаре (граф структурный) · O2 — in-place
поверх реального дерева = дефолт (живой/интерактивный/инкрементальный путь; git-режим держит набор чистым
без копий); materialize — только бенч-костыль для несговорчивых чужих тулов (§1.6) · O3 sha-256 full ·
O4 JSON-spec · O5 включать не-код .md · O6 git-биндинг (перечисление/провенанс/diff; идентичность = sha256).
Детали — в дизайн-доке. Готово к реализации.
Цель: перестать заявлять непроверенное. Правило вехи — утверждение в репозитории либо проверяется CI, либо убирается. Поднято из «Отложено → Дистрибуция» замером готовности к выпуску, а не обкаткой.
Что было: .github/ не существовало. 512 тестов гонял только я, руками, на одном интерпретаторе,
в одном каталоге. При этом README говорит «deterministic» и «512 tests green», CONTRIBUTING — «keep the
full suite green», pyproject.toml — requires-python >=3.10. Ни одно из четырёх ничем не держалось.
-
M20.1 Замер: заявленный диапазон Python — по чистому venv на версию, один и тот же коммит (
b87814b).>=3.10оказалось ложью: 3.10 — 13 failed / 499 passed; 3.11 — 4 failed / 508 passed; 3.12 / 3.13 / 3.14 — 512 passed каждая. Разложение чистое: 9 падений на 3.10 — нетtomllib(стдлиб с 3.11), и под ними лежал R1-C27; 4 падения на 3.10 и 3.11 — фикстураhardpkg/pep695.pyнаписана синтаксисом 3.12. Причём два из этих четырёх — R1-C23, работающий правильно: файл уезжает вprovenance.inputs.skippedсreason: "syntax", диагностика поднимаетunread_inputs, иtest_a_clean_tree_says_nothingпадает потому что дерево честно не чистое. -
M20.2
requires-python→>=3.11(D1) — измерено, а не предположено. 3.10 не выкупается зависимостьюtomli: у codemap ровно три рантайм-зависимости, и тратить одну на интерпретатор, у которого upstream-поддержка кончается в октябре 2026, — платить аренду за версию, на которой никого нет. Правка на две строки, если кто-то реально попросит. -
M20.3 Свойство «интерпретатор, на котором ты запущен» (D3) — четыре теста завязаны на
sys.version_info, плюс новый тест, который гоняется везде и проверяет само свойство: файл, синтаксис которого текущий интерпретатор не принимает, назван с причиной, а не выброшен молча. Свойство было истинным всегда и не было записано нигде: codemap разбирает мишеньast'ом того интерпретатора, на котором работает, — codemap на 3.11 не прочитает 3.12-мишень целиком. -
M20.4 Метаданные пакета — было пусто: ни
License-Expression, ниProject-URL, ни классификаторов, ни ключевых слов, ни автора. Машиночитаемо колесо не заявляло лицензии вообще, хотяLICENSE— MIT. Добавлено всё перечисленное; README-секция Install переписана под читателя, который ничего не клонировал (раньше там был толькоpip install -e .— инструкция клонировать, на странице, чей смысл — поставить без клонирования). -
M20.5 CI (GitHub Actions) (D4/D5) — четыре джобы, каждая прибита к утверждению, которое кто-то может прочитать в репозитории:
tests(матрица 3.11–3.14) держит «512 tests green» иrequires-python;determinism— заголовочное свойство (сборка дважды, побайтовое сравнение, включая блокprovenance);wheel— что отгружаемый артефакт ставится и работает вне дерева исходников (не проверялось никогда);interop—readtagsиscip, чьи два теста скипались в каждом прогоне за всю историю проекта. GitLab-зеркала CI не получает: issues живут на GitHub, GitLab — пуш-зеркало ради охвата, а два пайплайна — два места, где красная сборка может спрятаться. -
M20.6 Версия 0.0.2 → 0.0.3 (D6) — серия остаётся
0.0.xпо явному решению владельца.0.1.0должна значить «опубликовано под именем, с API, на который можно опереться снаружи»; ни того, ни другого пока нет. -
M20.7 CI нашёл дефект в первом же прогоне ✅ — и не тот, который искали. Все четыре джобы
testsупали сModuleNotFoundError: No module named 'tests'на коллекции: четыре модуля делаютfrom tests.frozen import frozen, а это требует корня репо вsys.path.python -m pytestкладёт туда cwd, голыйpytest— нет. То есть сьют проходил ровно тем способом, которым его документирует CONTRIBUTING, и падал тем, которым его набирает большинство. Воспроизведено локально одной строкой (.venv/bin/pytest -q→ 4 errors). Починено в[tool.pytest.ini_options] pythonpath = ["."]— обе формы вызова работают. Мелочь по последствиям и показательная по существу: непроверяемое утверждение расходится с реальностью ровно там, где его никто не пробовал, и это тезис всей вехи. Заодноinteropупал наgo install: уscipвgo.modестьreplace-директивы, и этот путь установки отказывает — заменено на релизный бинарь (пинv0.9.0), Go-тулчейн больше не нужен. -
M20.8 «зелёный прогон из скипов» — и он был наш собственный ✅ — второй прогон дал комфортное
474 passed, 55 skippedпротив локальных528 / 2. 53 скипа — «bquant sibling repo not present»: десятая часть сьюта это dogfood против реального внешнего пакета, который тесты ищут рядом с чекаутом, а у свежего раннера соседа нет. Ровно то, что джобаinteropнаписана отказываться принимать, — значит отказываемся и здесь: мишень чекаутится, и джоба падает, если эти тесты всё-таки скипнулись. Пин на коммит, не на ветку: утверждения кодируют факты об API мишени (сигнатура, депрекация), поэтому слежение за её HEAD красило бы CI codemap по причинам, к codemap отношения не имеющим, — тот самый сдвинувшийся вход, который стоил проекту дня и породил R1-C25. Устаревание пина — намеренная цена, а его подъём это осознанное действие с диффом. Плюс одно настоящее падение:test_resolve_adapter_mode_skips_routerпротиворечил докстрингу собственного модуля («no external tool is needed») — брал настоящий cocoindex, аresolveгейтит наis_available(), то есть на бинарьcccв PATH. Проходил на машине разработчика, у которой он случайно есть, и упал в первый же раз, когда запустился где-то ещё. Доступность застаблена: правило под тестом — диспетчеризация, а не установка. -
M20.9 Приёмка: CI зелёный, и цифры сошлись ✅ — прогон
33072689687, все 7 джоб success.testsна каждой из 3.11–3.14: 527 passed, 3 skipped (три опциональных внешних CLI) против локальных 528/2 — расхождение ровно наccc, которого нет на раннере.interop: 19 passed, 0 skipped —test_r1c2_ctagsиtest_scip_exportисполнились впервые за всю историю проекта и прошли против настоящихreadtagsиscipv0.9.0.determinismиwheelзелёные с первого прогона. -
M20.10 Имя дистрибутива:
codmap✅ (D7, решение владельца 2026-08-27) —codemapна PyPI занято (проверено: 404 наcodmap, 200 наcodemap). Двигается только дистрибутив: команда, импорт и репозиторий остаютсяcodemap. Шов (pip install codmap→codemap build) вынесен в первую строку README, а не оставлен на самостоятельное обнаружение. Неочевидное последствие — провенансное:provenance.pyбрал свою версию черезversion(TOOL_NAME), и переименование заставило бы это броситьPackageNotFoundError, который существующий обработчик превращает вNone— версия молча исчезла бы из каждого графа, и графы разных релизов снова стали бы неразличимы. Это ровно тот гэп, который закрывал R1-C25, заново открытый упаковочной правкой. Имена разведены:TOOL_NAME = "codemap"(идентичность в графе; не двигается — иначе все существующие 0.12-графы станут несравнимы из-за детали упаковки) иDIST_NAME = "codmap"(только дляimportlib.metadata). Два теста держат пару: один проверяет, что версия реально резолвится (молчаливыйNone— регрессия), второй — чтоDIST_NAMEсовпадает сnameвpyproject.toml. Проверено на собранном колесе:codmap-0.0.3ставится, командаcodemap, провенанс{'name': 'codemap', 'version': '0.0.3'}. Тесты 528→530. -
M20.11 ОПУБЛИКОВАНО ✅ (2026-08-27, по прямому указанию владельца) —
codmap0.0.3, первый выпущенный релиз проекта. Собрано из чистого дерева на запушенном коммите5ba1158,twine checkPASSED на обоих артефактах, выгружено twine. Проверено тем же способом, что и всё остальное в этой вехе: установка из PyPI в чистый venv, запуск из каталога без исходников codemap — 31 узел, 41 ребро, схема 0.12,provenance.tool={'name': 'codemap', 'version': '0.0.3'}безcommit(верный ответ вне чекаута). Страница несётlicense_expression: MIT, пять project-URL, 12 классификаторов,requires_python >=3.11. Тегv0.0.3.
Релизы остаются ручными — это решение, а не отсрочка (владелец, 2026-08-27). Триггер по тегу с
trusted publisher предложен и отклонён. Порядок выпуска — тот же, что дал 0.0.3: чистое дерево на
запушенном коммите → uv build → twine check dist/* → twine upload dist/* (~/.pypirc, [pypi]) →
проверка установкой из PyPI в чистый venv → тег vX.Y.Z (через git tag -F файл, не -m:
бэктики в двойных кавычках bash съедает как подстановку команд — наступили на это при выпуске 0.0.3).
На такой частоте автоматизация была бы лесами вокруг действия, выполняемого пару раз в год, а CI и так
проверяет почти всё, что перепроверял бы релизный workflow: сьют на четырёх интерпретаторах и колесо,
собранное, установленное и запущенное вне дерева исходников.
Про pip show — чтобы это потом никто не «чинил». Под pip < ~25 pip show codmap печатает пустые
License: и Home-page:. Ничего не потеряно: в метаданных есть License-Expression: MIT и пять
Project-URL (проверено на опубликованном артефакте), а pip 26.2 печатает оба поля правильно.
Это пробел отображения в старом pip, а не дефект упаковки. Сделать те две строки непустыми на старом
pip = откатить метаданные: PEP 639 запрещает совмещать License-Expression с устаревшим свободным
License или классификатором License ::, а Home-page из современной таблицы [project] вообще не
задаётся. Пустой Author: — та же безобидная причина: PEP 621 сворачивает authors = [{name, email}]
в Author-email: kogriv <…>, где имя и лежит.
Дизайн: docs/design/release_engineering.md. Гэп: gaps/unverified_claims_2026-08-27.md.
Пользовательские доки: docs/ci.md. Смежное: R1-C27 (дефект, вскрытый M20.1).
R1 закартировал поле сверху вниз; R2 идёт по каждому тулу с hands-on-замером на общей мишени
(bquant) → карточка на тул в research/tools/ + сводка research/comparison.md (матрица покрытия +
качество). Конвенция (codemap-native, на принципах tgsh) — в research/README.md.
Решения (2026-08-02): конвенция — своя, codemap-native (заточка под граф/замеры); hands-on всем релевантным тулам (не только прямым конкурентам); пилот — graphlens-mcp (ближайший близнец + у автора есть 936-прогонный бенч) для локировки формата.
Принципы (из tgsh): разбор ДО постройки соответствующей R1-C; «приходим с измерениями, а не с приговором» (авторы — потенциальные соавторы); лицензии (читать/учиться — да, копировать код без лицензии — нет); честная граница «что не проверяли» обязательна.
Общий task-set (ответ codemap = эталон): T1 где определён (analyze_zones) · T2 callers
(MACDZoneAnalyzer) · T3 impact (MACDZoneAnalyzer) · T4 что сломается при смене сигнатуры
(analyze_zones) · T5 архитектура/слои. Метрики: корректность · стоимость (токены/tool-calls) ·
латентность · детерминизм.
- [~] R2.0 Конвенция + каркас ✅ (2026-08-02) —
research/README.md(конвенция),research/tools/README.md(шаблон карточки),research/comparison.md(сводная матрица, засеяна из R1/R1.5 на desk-уровне). - R2.0.1 Харнесс бенч-скоупа ✅ (2026-08-03) — единый источник правды для входа бенча.
research/tools/_scope/bquant.scope.json(spec: 6 каталогов, venv исключён, сexpected-блоком) +materialize.py(переиспользуетcodemap.scope.resolve_scopeиз M19.A → копирует РОВНО файлы манифеста → staging несёт тот жеscope_id; self-verify round-trip). codemap сам — in-place; материализация только для venv-trap тулов. Канонический бенч-скоуп зафиксирован:scope_id sha256:300e0a01…5e47d2,bquant@cb89a24, 280 файлов (207 .py / 73 .md; core 90 / tests 81 / docs 56 / research 30 / examples 12 / scripts 11). Поле Scope добавлено в шаблон карточки (tools/README.md) иcomparison.md(якорь парити); graphlens-карточка ретро-проставлена (с честной пометкой: фактический прогон — на почти-идентичном ад-хок staging 285 файлов, +5 генерённыхdocs/_build). Проверено:materialize→ 280 файлов, verify== canonical ✓. - R2.1 Пилот: graphlens-mcp ✅ (2026-08-02, переизмерено 2026-08-03) — полный hands-on разбор.
Карточка
research/tools/graphlens.md. Итоги: (1) баг скоупа — игнорит.gitignore, отсекает venv по хардкод-именам →venv_bquantутянул весь venv (>3ч45м/9ГБ); воркэраунд — чистое дерево/пакет. (2) ИСПРАВЛЕНИЕ вывода первого прохода: «T2/T3 пусто» было нашим окружением, не слабостью тула — graphlens бандлитty, но ищет его черезshutil.which("ty"), аuv tool installне кладёт бандл-bin/на PATH → тихий degrade в tree-sitter-only. Фикс:tyна PATH →resolver_status: ok. (3) Переизмерено на том же staging: type-resolved индексация 2м20с/424МБ/31МБ БД/32399 узлов/55691 ребро (degraded было 12с/17.5МБ/16796).relationsработает —MACDZoneAnalyzer: 9 callers+1 callee+2 refs (тесты авто-скрыты), ≈ codemap-овские 12 не-тестовых из 31. T4/T5 инструментов реально нет. Вердикт learn (достойный peer, а не «пусто»); питает R1-C13 (бенч должен проверятьresolver_status==ok) и R1-C14 (дифференциаторы: детерминизм, single-call provenance-impact, no-LSP-dependency, layout-robustness). - [~] R2.2… остальные — 🟡 в работе.
- [x] GitNexus ✅ (2026-08-06, hands-on через новый харнесс R2.0.1) —
research/tools/gitnexus.md. v1.6.9, TS/Node, tree-sitter + LadybugDB + локальные ONNX-эмбеддинги, 14 языков, PolyForm-NC. Измерено на R2-скоупе (materialized staging,scope_idсверен): T1 ✅ (ambiguity как у нас), T2 ◐ (import fan-in, не call-sites), T3 ✅ (транзитивный import-closure + risk + epistemic), T4 ✖ (нет call-contract), T5 ◐ (cycles+кластеры+flows, без coupling/god-objects). Детерминизм ◐: ответ байт-идентичен (clean-room A/B), артефакт — 123 МБ бинарный LadybugDB (не diffable); re-analyze без clean не идемпотентен. Установка 1.7 ГБ node_modules. Вердикт learn (сильный, смежный peer — комплементарен, не конкурент). Питает R1-C13 (epistemic/confidence-метки), R1-C14 (дифференциаторы: MIT vs NC, diffable JSON vs бинарь, provenance-impact, no-git), R1-C15 (clusters+flows → living docs). Новый gap: семантический поиск + flow/community-нарратив. Build-story #2 вresearch/positioning.md(«The one that does more, and why that's fine»). - [x] cocoindex-code (ccc) ✅ (2026-08-16, hands-on на R2-скоупе) —research/tools/cocoindex-code.md. 0.2.41 на cocoindex 1.0.20, Apache-2.0, Python+Rust-движок, tree-sitter-чанкинг, локальные ST-эмбеддинги (snowflake-arctic-embed-xs— та же модель, что у GitNexus), встроенный LMDB+SQLite (без БД-сервиса, без API-ключа). Развод: CocoIndex (ETL-фреймворк, часто Postgres+pgvector) vs cocoindex-code (сам CLI, embedded store) — карточка про второй. Измерено: 280 файлов → 6403 чанка; T1 ◐ (semanticsearchдаёт релевантный спред, деф не #1; точный деф — через bundledccc grep, tree-sitter); T2–T5 N/A (векторный индекс, нет графа/вызовов/impact/арх); concept-query блистает (swing pivot points→ точноstrategies/swing/pivot_points.py, чего codemap не умеет); детерминизм ◐ (ответ байт-идентичен, артефакт — бинарный LMDB/SQLite); инкрементальная переиндексация ≈ 1с (content-hash — тезис движка). GPU заблокирован только на Pascal (torch 2.13/cu130 не несёт cubin под sm_61 → CPU-only на 1080 Ti; onnxruntime GitNexus'а на sm_61 ехал — другой рантайм); на RTX 3070 (sm_86) замерено 2026-08-22: холодная сборка 48с против 216с CPU на той же машине (~4.5×). Вердикт wrap (opt-in семантик-адаптер) + learn (инкрементальный движок): первый лицензионно-чистый (Apache-2.0) семантик-поиск, пригодный к обёртке за роутером R1-C16 (там, где GitNexus толькоroute, но неadaptиз-за NC). Питает R1-C16 (первый adapter-кандидат семантики), R1-C9 (content-hash инкрементальность — рабочее доказательство), R1-C6 (relevance). - [x] CodeGraph (colbymchenry) ✅ (2026-08-28, hands-on на R2-скоупе) —research/tools/codegraph.md. v1.6.0 (npm), MIT, Rust-ядро + tree-sitter (20 языков), SQLite+FTS5, 283 МБ установка одной командой. Самый принятый тул из всех разобранных: 68 420 ★ / 4 362 форка, репо семь месяцев от роду. Измерено: 207 из 280 файлов скоупа (.mdвне модели) → 5 113 узлов / 15 247 рёбер за 1.4 с — ~9× быстрее нашего fast-тира и ~68× быстрее deep. T1 ✅ оба определения на тех же строках, что и у нас, + inlinesignature+ нечёткие соседи; неоднозначность видна как ранжированный список, но как неоднозначность не помечена. T2 ✅ символьный: 58 вызывающих, и наши 57 — полное подмножество; единственный лишний —assert isinstance(...), то есть ссылка, посчитанная вызовом (его же--helpговорит "call"). T3 ✅ 89 затронутых на глубине 2, но плоским нетегированным списком — у нас 69 рёбер, каждое сtype(calls 60 / references 9),root(core 3 · docs 7 · examples 1 · scripts 2 · tests 56),distanceи агрегатнымrisk. T4/T5 ✖ — ни контракта аргументов, ни слоёв/циклов; за четыре hands-on карточки T4 и T5 не закрыл никто. Детерминизм ◐: структура воспроизводится точно (clean-room A/B: те же счётчики, тот же размер БД), артефакт — нет (разный md5), аqueryнесёт на каждом узле стенные часыupdatedAt— ответ меняется, когда в коде не изменилось ничего. Ловушка:--limitпо умолчанию 20, file-строки сортируются первыми, флагаtruncatedнет — дефолтный ответ читается как file-import-модель, хотя настоящая символьная. Вердикт learn-only (strong). Питает M3.2 (ставит watcher: debounce 2 с, инкремент 121 мс — переоценка вверх), R1-C13 (партиальность бывает не только от приближения, но и от лимита), R1-C6 (2-е появление скоринга), R1-C14 (снял с нас скорость, лицензию и мультиязычность как дифференциаторы). Отдельно ценно — не техника: проект публикует ось, на которой проигрывает (≈80% больше резидентного контекста), и отозвал собственные ранее опубликованные цифры, обнаружив загрязнение контрольной группы в 26 прогонах из 28. - [x] OntoIndex ✅ (2026-09-01, hands-on на R2-скоупе) —research/tools/ontoindex.md. 2.2.0, TS/Node 22.12–25.x, tree-sitter + LadybugDB, 14 языков, AGPL-3.0, потомок GitNexus (атрибуция вNOTICE), 489 МБ установки. Скоуп потребовал нового шага: дерево догфуда ушло далеко отcb89a24, поэтому staging собран изgit archive cb89a24(read-only по чужому репо) и уже из него материализован —scope_idсошёлся. Всем последующим карточкам делать так же. Индекс: 280 файлов (279 разобрано, 1 за размерным капом) → 10 745 узлов / 20 232 ребра / 254 кластера / 300 потоков за 49.1 с, 1.0 ГБ RSS, 62 МБ стор (codemap: 4 225 / 11 502, 11.4 с fast / 111 с deep, 5.4 МБ JSON; счётчики несравнимы — у них markdown-заголовки, Leiden, шаги процессов и дерево саммари, у нас слойcolumn). T1 ✅ оба определения +status: "ambiguous"родной меткой (второй тул из измеренных, который неоднозначность называет, а не ранжирует); ноstartLine0-based — 121/717 там, гдеdefна 122/718. T3 ✅ самое сильное совпадение за все карточки:impact --include-testsна глубине 1 даёт ровно наши 57 вызывающих дляMACDZoneAnalyzerи ровно 78 дляget_sample_data— множества идентичны, ни одного элемента с чьей-либо стороны отдельно, и это движок, не делящий с нами ни строки кода. По умолчанию тесты исключены (55 против 149). T2 ◐ и это контрпример серии:contextрежетincoming.callsна 30 и при этом пишетcontextCompleteness.truncated: false— 30 из 57 и 30 из 78; два индекса одного и того же входа держат разные 30-элементные подмножества. Под капом точен (28 = 28, 6 = 6). T4 ✖ структурно (проверено Cypher'ом по стору: у узлов нет параметров, у рёбер — аргументов); пятая карточка подряд, где T4 не закрывает никто. T5 ◐audit(нужен git) даёт правильные разделы, но «Import Cycles (6)» смешивает кольцо из 38 markdown-страниц, два цикла вызовов и модульные циклы, отрисованные по basename, против наших 1 жёсткого + 44 ленивых, названных полными id и разделённых по признаку «ломается ли на импорте»; таблица coupling ключуется именем сообщества с дублями иCa 0во всех строках. Найденный дефект (репортить наверх):report hubsиreport surprising-connectionsпечатают «no hubs found (index missing, empty graph, or no connected nodes)» на графе в 10 745 узлов — причина вwarnings: вендоренный парсер Cypher не понимает отрицание метки (NOT n:Label), подтверждено прямым запросом. Exit 0, в JSON"hubs": []. Отказ, отрисованный пустым результатом. Детерминизм ◐ и он расслаивается: граф не воспроизводится (10 745/20 232/254 против 10 747/20 227/256 на байт-идентичном входе),impactстабилен по множеству и нестабилен по порядку (384 позиционных расхождения),contextне стабилен ни по чему. Честность — лучшая в поле там, где держится: на каждом ребре стоит способ резолва и градуированная уверенность (same-file0.95 ·import-resolved0.9 ·global0.5 · markdown-импорты 0.8),report --helpсам объявляет свою потерю точности и маршрутизирует к авторитетной операции (isRankedDiscovery: trueв JSON), а пропущенный по размеру файл повторяется вwarningsкаждого ответа. Вердикт learn (strong) — AGPL закрывает иwrap, иintegrate. Питает R1-C13 (метки на ребре, а не на ответе), R1-C28 (сильнейший контрпример:truncated: falseна срезе 78→30), R1-C14 (детерминизм поднимается как дифференциатор; «эпистемическая честность» больше не наша одна). Новые кандидаты: R1-C39 (причина резолва + уверенность на ребре), R1-C40 (earliest_broken_stepдля потоков). - [ ] остальные — rag_for_git, Understand-Anything, CodeSlicer, ast-index, Graphify, grafema, CodeWiki, Foglamp (desk где SaaS/не воспроизводится — с честной пометкой). Порядок — по близости к codemap и по связи с R1-C. Sentrux понижен до desk (2026-08-28, по факту): ★2919, MIT, Rust — но создан 11.03.2026, последний push 19.03.2026, то есть восемь дней коммитов и пять месяцев тишины. Hands-on по незаброшенному тулу дороже; идеюrules.toml+ health-delta читаем из исходника в R1-C3.
Первый прогон codemap как продукта в бою — ИИ-агент (Claude Code) через MCP по графу bquant.
Инструменты отработали (architecture поймал analysis↔indicators; impact/call_contract дали карту для
смены сигнатуры; resolved.ambiguous прошёл через MCP). Находка эргономики канала:
- F22 (MCP/Workflow) — тяжёлый payload на хабах. ✅ (2026-07-30, без схемы) На
MACDZoneAnalyzer:impactвозвращал 68 ссылок + дублирующий полныйmarkdown,call_contract— 61 запись. Фикс: MCP-обёрткиimpact/call_contractкомпактны по умолчанию —impactбезmarkdown+limit=40на плоский список refs (by_root-счётчики полны),call_contractlimit=30;full=trueвозвращает всё. Underlying-ops/CLI не тронуты. Замер на хабе: impact −65% (32.7k→11.6k), call_contract −49% (17k→8.8k).serve/mcp_server.py_compact_impact/_cap_list. +4 теста (7→11 MCP). - F23 (Query-surface) —
impactне принимал полный/canonical id. ✅ (2026-07-30, без схемы) На реальной задаче (bquant #110)impact('bquant.analysis.zones.models.SwingPoint')→[]. Гипотеза «конструктор не ловится» ОПРОВЕРГНУТА при воспроизведении: инстанциация класса ловится — уSwingPoint9 inbound calls-рёбер,references_to= 9. Настоящая причина: op/рендерimpactрезолвили вход только по короткому имени (find(sym)), поэтому полный id (ровно то, что агент получает назад изquery/search) не матчился ни во что → пустой blast-radius. Молчаливо неверно (как F13, но для impact). Фикс:Query.impact_targets(name_or_id)— node-id→сам, короткое имя→все матчи (фан-аут сохранён), иначеcanonical(re-export), иначеwhere_defined; 使用 в_op_impactиrender_impact. Проверено: full id и re-export → 10 refs (было 0), короткое имя без изменений. +2 теста. Урок: воспроизводить до фикса — записанная гипотеза (extraction) оказалась неверной, реальный баг был в input-резолве serve-слоя.
Кандидаты из обкатки архитектуры 2026-07-30 (ось A9, findings F18–F21) — ✅ ЗАКРЫТЫ (M16, 2026-07-30)
Обкатка A9 (gaps/architecture_dogfood_2026-07-30.md): роль архитектора — «форма системы целиком».
Было только report dependencies (циклы + top-imported). 4 гэпа, все Query-surface/Workflow, без
схемы (поверх import-графа/calls/contains/провенанса), +7 тестов (116→123); serve report-kinds 4→5,
ops 20→21. Реальная находка на bquant: слоевое нарушение analysis ↔ indicators (взаимозависимость).
- F18 (Query-surface/Workflow) — слои + направление + нарушения. ✅ (M16, 2026-07-30, без схемы)
Query.layers()→ {слои (компонент под пакетом), межслойная матрица, violations order-free = слоевые пары с рёбрами в обе стороны — без хардкода порядкаcore<analysis}. На bquant: 8 слоёв, нарушениеanalysis↔indicators. - F19 (Query-surface) — coupling / instability. ✅ (M16, 2026-07-30, без схемы)
Query.coupling()→ per-module Ca (кто зависит от меня) / Ce (от кого завишу) / I=Ce/(Ca+Ce).logging_configCa94 I0.01 (стабильный лист),exceptions/config/nbI0.00 (стабильное ядро). - F20 (Query-surface) — god-объекты / хотспоты. ✅ (M16, 2026-07-30, без схемы)
Query.hotspots()→ god-классы (методы≥порог:ZoneVisualizer35,NotebookSimulator23,ZoneAnalysisPipeline20) + call-хабы (in+out) с флагом pervasive (логгер/util — ожидаемый шум, не риск;ContextualLogger.info105 помечен, реальные хабыanalyze_zones48 выделяются). - F21 (Workflow) — синтез: architecture overview. ✅ (M16, 2026-07-30, без схемы)
serve/architecture.py:build_/render_architecture(циклы+слои+coupling+хотспоты в один вид); report kindarchitecture(CLIcodemap report architecture+ serve report); serve oparchitecture(структурный). Проверено на живом графе — читаемый одностраничный обзор формы системы.
Кандидаты из обкатки diff/change-review 2026-07-30 (ось A11, findings F16–F17) — ✅ ЗАКРЫТЫ (M15, 2026-07-30)
Обкатка A11 (gaps/changereview_dogfood_2026-07-30.md): вход ревьюера — дифф (файл+строки), не имя.
Два разрыва, оба в query/serve-слое (без схемы), +13 тестов (103→116); serve 18→20 ops.
- F16 (Query-surface) — резолвер локация→символ. ✅ (M15, 2026-07-30, без схемы)
Данные containment (
file+lineno+endlineno) были, поверхности не было;searchматчит только подстроку id.Query.symbol_at(file,line)(внутренний-первым, fallback к модулю для кода между def'ами) +symbols_in_range(file,s,e)(per-line дедуп); serve oplocate({file,line}|{file,lines}). - F17 (Workflow) — агрегация ревью change-set. ✅ (M15, 2026-07-30, без схемы)
Было: 4 символа = ~20 ручных вызовов + склейка.
serve/review.py:build_review(hunks|symbols)→ сшитое досье (per-symbol callers/call_contract/columns/consumers_by_root) + union blast-by-root + risk-ранг (синтез из fan-out/cross-root/contract-sites/dataflow — R3 сложился бесплатно) + unresolved-хунки (ничего не теряется молча);render_reviewmarkdown; serve opreview; CLIcodemap review <diff>с парсером unified-хунков (parse_unified_diff, new-side диапазоны). Проверено на реальномgit diff(7 хунков→10 символов, high-risk первыми, blast core 32/tests 27).
Границы (не гэпы, в бэклог): added/deleted символы → двух-графовый diff (rebuild@base vs @head),
отложено с M3.3 (резолвер деградирует к объемлющему узлу, added не выдумывает); consumer-руты
(tests/examples) file НЕ несут → дифф по тесту не локализуется (core-ревью — 80% случая).
Кандидаты из обкатки soundness/trust 2026-07-30 (ось B1, findings F14–F15) — ✅ ЗАКРЫТЫ (M14, 2026-07-30)
Обкатка B1 (precision/recall трёх приближений + canonical, gaps/soundness_dogfood_2026-07-30.md):
сверка карты с исходником bquant. Итог: два «страшных» приближения — фактически точны (мост M7:
0 false-exact, веер честен; implements: 12/12 истинны, recall 100%); два реальных дефекта доверия —
молчаливые — закрыты вехой M14. +5 тестов (98→103).
- F14 (Soundness/Workflow) — молчаливая
canonical-дизамбигуация. ✅ (M14, 2026-07-30, без схемы) Голое короткое имя (calculate— 25 defs,main— 19) резолвилось в 1 узел по кратчайшему id, без сигнала — реляционный op уверенно отвечал про не тот символ (вплоть до тест-мокаMockSwingStrategy.calculate_global).Query.canonical_infoвозвращает{input,id,ambiguous, alternatives}(ambiguous⇔ ≥2 кандидата в ничью по path-сигналу);canonicalделегирует ему; Session_canonпишет резолюцию,handleкладёт блокresolvedв конверт, когда выбор был неоднозначным (F14) или переписал вход (re-export, F13); opresolveотдаёт полный инфо-дикт. На bquant:callers('calculate')→resolved.ambiguous=True, 24 альтернативы;ZoneDetectionStrategy(1 def) →ambiguous=False(ложной тревоги нет). Фикстура dispatchpkg (runв Alpha/Beta/Protocol) + 4 теста. - F15 (Soundness/misleading-label) — column node-set = 71% payload-шум. ✅ (M14, 2026-07-30, схема 0.9)
Правило F6 «dict-literal ключ = writer» ловило каждый результат-словарь: из 1007
column-узлов 71% — dict-literal-only ключи (adf_statistic,n_simulations,text.color), не колонки; агрегатcolumns()вводил в заблуждение.extract/dataflowпишет access-form: узел несётextras.subscripted(был ли ключ хоть разx['k']), ребро —extras.access(subscript|dict-literal).Query.columns(subscripted_only=True)по умолчанию отдаёт реальный column-set (~30%: 300/1007); точечныйcolumn('macd_hist')и продюсер-ребро F6 не тронуты; opcolumnsпринимаетall=trueдля полного over-set. Фикстура flowpkg (meta= dict-only) + 2 теста.
Обкатка рабочего цикла агента через serve (gaps/agent_workflow_dogfood_2026-07-29.md): 5 гэпов
Workflow/Query-surface. Все закрыты одной вехой M13 (serve/query-слой, без схемы), +7 тестов (98/98).
- F13 (Precision/Workflow) — реляционные ops резолвят re-export/короткое имя. ✅
Query.canonical; Session-ops (implementers/family/callers/callees/call_contract/columns_of/source) резолвят вход через_canon; opresolve.implementers('…detection.ZoneDetectionStrategy')→ 5 (было []). - F12 (Workflow) — хендл к исходнику. ✅
file/linesвmatches(и в text-выводе); opsource(сниппет по--source-root, best-effort; overlay-узлы → location-only с нотой). - F9 (Query-surface) — discovery/поиск/обзор. ✅ op
search(подстрока+kind+limit), opfamilies(Protocol → члены).search ZoneDetection→ 5 классов (было пусто). - F11 (Query-surface) — обратный dataflow. ✅
Query.columns_of(func)+ opcolumns_of+ полеcolumnsв query функции.columns_of(extract_zone_features)→ reads [atr, close, …]. - F10 (Workflow) — extension-рецепт. ✅
registered_as(decorator+key) в query класса + text «register with: @register('zero_crossing')»; opfamiliesотдаёт рецепт по каждому члену.
Обкатка на 5 непокрытых осях (gaps/deep_dogfood_2026-07-29.md): 3 новых гэпа + 2 подтверждения.
Не блокеры; порядок — по «дёшево×ценно». Каждая находка с категорией по причине (диктует форму фикса).
- F8 (Representation) — провенанс-осознанный
report dead-code. ✅ (M8, 2026-07-29)Query.orphan_modules(root=)+orphan_modules_by_root(); отчёт показывает core-орфаны как сигнал (8), consumer-entrypoint'ы (116: tests 75/examples 11/research 24/scripts 6) свёрнуты с пояснением «orphan по природе, не dead code». Serve+query, без схемы. +2 теста (65/65). - F4 (Query-surface) — вид семейства реестр+Protocol. ✅ (M9, 2026-07-29, схема 0.6)
extract/dispatch.add_family_linksсинтезируетimplements-рёбра (impl→Protocol) по таблице семейств, data-driven матч (токен ⊂ имя Protocol; безтокенный — по имени реестра). 12 рёбер на bquant. Query:implementers/implements/family_siblings+ вывод вquery; mermaid class — realization<|..(семейство больше не пустое); RAG-чанк несёт Implements/Implemented by. Фикстураdispatchpkg/base.py(ThingProtocol) + test_m9_family.py (6 тестов). 71/71. - F3 (Representation) — класс-чанк агрегирует call-соседей методов. ✅ (M10, 2026-07-29)
serve/rag._methods_calls: union внешних callees методов класса →neighbors.calls_via_methods(каждый target с меткойvia <метод>) + текст «Methods call: …». Serve-only, без схемы. MACDZoneAnalyzer класс-чанк теперь показывает шовanalyze_zones (via analyze_complete_modular)(+ detect_zones/with_indicator/build) — делегирование в pipeline видно без чтения методов. +1 тест (72/72). - F7 (Representation+Precision) — арг-контракт на call-site. ✅ (M11, 2026-07-29, схема 0.7)
behavior._arg_shape/_arg_contract:calls-рёбра несутcallsites(сколько выражений вызова схлопнуто) +posargs/kwargs/splat(наблюдённая форма аргументов); захват и в behavior-проходе, и в consumer-сканеroots.py.Query.call_contract+ секция «Call-site contract» вreport impact. На bquant get_indicator_params: examples ×2 (схлопывание снова видно), все «1 positional». Фикстура argpkg + test_m11 (5 тестов). 77/77. - F6 (Extraction) — dataflow по строковым ключам. ✅ (M12, 2026-07-29, схема 0.8)
extract/dataflow.add_dataflow:column-узел на строковый ключ +writes(subscript-store и dict-литерал-продюсер{'k':…}) /reads(subscript-load) рёбра function→column. Embedded-данные исключены.Query.column(name)/columns()+query <col>печатает producers/consumers. На bquant: 1007 колонок / 2381 рёбер;query macd_hist→ written byMACD.calculate, read by extract_zone_features + 4 визуализатора (было — пусто). Честно: over-set строковых ключей (dict- доступ тоже попадает). Фикстура flowpkg + test_m12 (6 тестов). 83/83.
- Мультиязычность — экстракторы Go (
go/packages, разборreplan), TS (ts-morph), C++ (libclang), Rust (rustdoc-json); tree-sitter как фолбэк (§12, §11). - Инкрементальность — контент-хеш + инвалидация (заимствуя Salsa/build-систему) (§15.3).
- Neo4j query-бэкенд — на system-scale графе (§4, §16).
- Система как граф — инфра/микросервис-экстракторы (IaC/OpenAPI/OTel), твои серверы (§16).
- Внешние тулы за адаптерами + реестр кандидатов (§13).
- Дистрибуция —
uv/pipxпакет; компилируемый бинарь (Nuitka) при нужде (§14). - Структурный разбор докстрингов (Google/NumPy-секции) для потребителя B (§10.2).
- Точный call-graph / sequence-виды — слой глубины (§7).
- M0 первым — тонкий срез доказывает конвейер, даёт пользу (вход для доки), проверяет форму.
- Каждая веха — сквозная и полезная (не «слой без выхода»).
- Расширять каталог §1 — только осознанно; «Отложено» не тащить в v1.
- Ядро/схема/CLI стабильны; новое (языки, бэкенды, виды) — аддитивно за адаптерами.
Живой документ. Отмечать [x], дописывать задачи по мере реализации.