diff --git a/docs/audit/2026-08-26-audit.html b/docs/audit/2026-08-26-audit.html new file mode 100644 index 00000000..fb5bafa8 --- /dev/null +++ b/docs/audit/2026-08-26-audit.html @@ -0,0 +1,272 @@ + + +
+ + +project-audit · 2026-08-26T16:33:49Z · read-only
+Every number below was produced by a command this run executed. What could not be measured is listed as such, never omitted and never counted as clean.
+First run. There is no earlier sidecar in this directory, so nothing is reported as closed or new — a diff against a run that never happened would be a claim about nothing. The next audit will have both columns.
+| version | — (no manifest version) |
|---|---|
| languages | — |
| package managers | — |
| monorepo | yes |
| submodules | 0 |
| CI | github-actions |
| deploy targets | compose, procfile |
| error telemetry | none found in manifests |
| tracked files | 1500 |
Remedy. rotate the credential at its issuer, then remove it from the tree; if it is in history, rotation is the fix and rewriting history is not
+Remedy. rotate the credential at its issuer, then remove it from the tree; if it is in history, rotation is the fix and rewriting history is not
+Remedy. rotate the credential at its issuer, then remove it from the tree; if it is in history, rotation is the fix and rewriting history is not
+Remedy. rotate the credential at its issuer, then remove it from the tree; if it is in history, rotation is the fix and rewriting history is not
+Remedy. rotate the credential at its issuer, then remove it from the tree; if it is in history, rotation is the fix and rewriting history is not
+Remedy. rotate the credential at its issuer, then remove it from the tree; if it is in history, rotation is the fix and rewriting history is not
+Remedy. rotate the credential at its issuer, then remove it from the tree; if it is in history, rotation is the fix and rewriting history is not
+Remedy. rotate the credential at its issuer, then remove it from the tree; if it is in history, rotation is the fix and rewriting history is not
+Remedy. rotate the credential at its issuer, then remove it from the tree; if it is in history, rotation is the fix and rewriting history is not
+Remedy. rotate the credential at its issuer, then remove it from the tree; if it is in history, rotation is the fix and rewriting history is not
+Remedy. rotate the credential at its issuer, then remove it from the tree; if it is in history, rotation is the fix and rewriting history is not
+Remedy. rotate the credential at its issuer, then remove it from the tree; if it is in history, rotation is the fix and rewriting history is not
+Remedy. rotate the credential at its issuer, then remove it from the tree; if it is in history, rotation is the fix and rewriting history is not
+Remedy. rotate the credential at its issuer, then remove it from the tree; if it is in history, rotation is the fix and rewriting history is not
+Remedy. rotate the credential at its issuer, then remove it from the tree; if it is in history, rotation is the fix and rewriting history is not
+Remedy. rotate the credential at its issuer, then remove it from the tree; if it is in history, rotation is the fix and rewriting history is not
+Remedy. rotate the credential at its issuer, then remove it from the tree; if it is in history, rotation is the fix and rewriting history is not
+Remedy. rotate the credential at its issuer, then remove it from the tree; if it is in history, rotation is the fix and rewriting history is not
+Remedy. rotate the credential at its issuer, then remove it from the tree; if it is in history, rotation is the fix and rewriting history is not
+Remedy. rotate the credential at its issuer, then remove it from the tree; if it is in history, rotation is the fix and rewriting history is not
+Remedy. rotate the credential at its issuer
+Remedy. rotate the credential at its issuer
+Remedy. rotate the credential at its issuer
+Remedy. rotate the credential at its issuer
+Remedy. rotate the credential at its issuer
+Remedy. rotate the credential at its issuer
+Remedy. rotate the credential at its issuer
+Remedy. rotate the credential at its issuer
+Deploy targets declared (compose, procfile) with no telemetry dependency in any manifest. A failure on a user's machine is invisible.
+languages= deploy=compose,procfile telemetry=[]
+Remedy. add an error reporter, or record the decision not to — the gap worth closing is that nobody wrote down which it is
+A probe that could not run returns blind, never clean. An empty section here would mean every probe answered — not that nothing is wrong.
| Probe | Phase | Why not |
|---|---|---|
| channel-divergence | prod | no version in a manifest to make a claim about |
| published-version | prod | no name+version in a manifest |
| Probe | Phase | Verdict | Note |
|---|---|---|---|
| secrets-tree | probe | finding | 20 credential pattern(s) |
| secrets-history | probe | finding | 8 in history |
| worktree | probe | clean | working tree clean |
| telemetry | prod | finding | no error reporting found |
| ci-present | prod | clean | CI configured: github-actions |
| docs-present | seams | clean | 7 documentation marker(s): README.md, docs, CLAUDE.md, CONTRIBUTING.md, CHANGELOG.md, docs/adr, ARCHITECTURE.md |
| gitignore-secrets | probe | clean | no credential-shaped file is tracked |
| channel-divergence | prod | blind | no version in a manifest to make a claim about |
| published-version | prod | blind | no name+version in a manifest |
Cold audit · read-only · nothing written
+2026-08-26 · after the 2026-08-25/26 remediation programme · production
+checkmydata-api v274, checkmydata-web v222, both healthy
One finding is worth acting on today, and it is the same shape as a defect +this programme fixed yesterday — which is why it is worth naming rather than filing.
+ +BILLING_ENABLED = set heroku config -a checkmydata-api
+STRIPE_SECRET_KEY = <UNSET> heroku config:get STRIPE_SECRET_KEY
+/pricing → 200 <meta name="robots" content="index, follow">
+/api/billing/plans → 200 returns the real plan catalogue
+subscriptions = 0 rows stripe_events = 0 rows
+ A signed-in visitor who clicks Upgrade reaches POST /api/billing/checkout,
+ which calls a Stripe API with no key. It degrades honestly —
+ billing_service.py:45-46 raises BillingError and the route returns
+ 400, not a crash — but the message the customer sees is
+ "Stripe is not configured (STRIPE_SECRET_KEY missing)": an operator's
+ diagnostic, on the money path, on a page search engines are invited to index.
RERANKER_ENABLED — a flag advertising a capability the runtime does not carry —
+ which yesterday's app/ops/capability_report.py was built to catch at every boot.
+ The gate makes exactly three claims (reranker_enabled,
+ chroma_embedding_model, chroma_server_url) and this is not one of
+ them. A mechanism built for a class of defect that does not cover the class's clearest
+ member is the interesting part.Remedy, in order of cost. Add the claim to CLAIMS —
+ billing_enabled asserted, stripe_secret_key provided — so every boot
+ says it. Then either unset BILLING_ENABLED until the keys exist, or give the
+ checkout route a customer-facing message. Whichever is chosen, the boot line is what stops
+ it being rediscovered.
The 2026-08-23 report named six such subsystems. Counted from production's +own empty tables, it is eleven. For all of them a green CI run is the only +evidence there is, and the first real user is the first integration test.
+ +| Subsystem | Evidence (0 rows in production) | Reachable by a user? |
|---|---|---|
| Billing | subscriptions, stripe_events | Yes — see A-1 |
| GA4 analytics | analytics_imports, all five ga4_*, vendor_credentials | Yes — the [1.16.0] headline feature has never ingested a row |
| Scheduled queries | scheduled_queries, schedule_runs | Yes |
| Notifications | notifications | Yes |
| Investigations | data_investigations | Auto-triggered — orchestrator_auto_investigate_enabled defaults on |
| Learning votes | learning_votes | Yes |
| Batch queries | batch_queries | Yes |
| Semantic layer | metric_definitions, metric_relationships | Yes |
| RAG feedback | rag_feedback | Yes |
| Validation feedback | data_validation_feedback | Yes |
| Project repositories | project_repositories | The table is unused entirely — its dead encrypted column was dropped yesterday (F-REPO-04) |
learning_votes is empty, and a5f6888 (#216) corrected how a
+ vote is weighed against a re-derivation. The reasoning holds and the tests are
+ real, but no human has ever voted on a learning in this deployment.
Remedy. None in code. Record the exercised/unexercised split beside + the board so priority arguments start from it.
+29 findings — 28 critical "private key committed", 1 medium
+profile: "languages": [] "managers": [] "telemetry": [] "version": null
+ "monorepo": true
+ It detected a monorepo and then read manifests only at the root, which has none.
+ backend/pyproject.toml and frontend/package.json were never opened.
+ That single gap produced the empty language and manager lists, the false telemetry
+ finding — Sentry is in both manifests and live in production — and both declared-blind
+ probes.
blind
+ and gave a reason. telemetry returned a finding instead, and the two
+ empty profile fields returned nothing at all. A probe that cannot see and says so is a
+ question; a probe that cannot see and answers anyway is a wrong answer with a clean
+ verdict attached.16 files matched -----BEGIN … PRIVATE KEY----- (17 of 20 hits in tests/)
+3 hits in source: a <textarea> placeholder ×2 and a `secretHint` string
+strict PEM test (matching END, no code in the span, >16 distinct chars):
+ tree → 1 candidate backend/tests/integration/test_security_rbac.py:302
+ history → 0 candidates across all 8 flagged commits
+ssh-keygen -y on that candidate → "invalid format"
+ The one survivor is high-entropy filler shaped like a key, used by
+ test_ssh_key_not_in_response to prove the API never echoes a submitted key
+ back. It is not a key.
(.*?)-----END spanned several unrelated tests
+ and accumulated 836 characters of code as "base64", which read as a second real key. The
+ strict form — matching END of the same kind, rejecting spans containing code —
+ removed it. A scanner and a verifier can fail identically.Each of these was a claim when it shipped. These are the measurements.
+ +08-26 00:00 UTC index_repo completed (record_index) 00:00 → 00:02
+08-26 00:02 db_index completed 00:02 → 00:27
+08-26 00:27 code_db_sync completed 00:27 → 00:28
+08-26 00:00 daily_sync completed 00:00 → 00:28
+08-25 22:00 index_repo completed 22:00 → 22:42 (42 min)
+
+stale run reaped: 0 R14: 0 R15: 0
+ Baseline from the 08-23 report: failing every day since 08-07, 64 of 70
+ failures with error='stale run reaped'. Before the fix any single step over
+ 300 s was killed while working; a 42-minute run across many steps is the direct
+ counter-evidence.
db_index started 00:42 CEST and was reaped at 00:51; Heroku released
+ v269 at 00:46 CEST — my own deploy restarted the worker mid-run. The process
+ was genuinely gone, so the reaper was correct. I can tell only because N3 recorded the
+ step.run | db_index | fatal | stale run reaped (step: fetch_samples) | 08-25 22:51
+run | daily_sync | fatal | stale run reaped (step: db_index) | 08-25 22:51
+ Before: 3 rows, newest 08-17, against 143 failed runs. The catalog now records what it + exists to record, and the step is what makes a concentration diagnosable.
+Three boots, three warnings: the configuration names a 768-d embedding model while
+ Chroma embeds at 384-d, and the vectors are not comparable. reranker_enabled
+ dropped out of the warnings because it is now unset. make config-drift → exit 0.
+ npm audit --audit-level=high → exit 0. Gate at 80% against a real 82%.
Disabling CLUSTERING_ENABLED left 149 code_clusters rows written
+ 08-25 06:53. They are read by the SQL agent — but the read is behind the same flag
+ (sql_agent.py:189-193), so nothing consumes them, and a re-enable deletes before
+ inserting (code_graph_service.py:404). Dead bytes, not stale data that is
+ trusted.
Both of these surfaced because the audit's own pull request would not +build. Neither was on any checklist.
+ +main is not protected, so yesterday's CI fix has no teethGET /repos/CheckMyData-AI/checkmydata-ai/branches/main/protection
+ → 404 "Branch not protected"
+ #223 made CI run on every pull request, including the stacked ones it used to skip. + That closed the blindness. It did not make the result binding: with no protection + rule, a pull request can be merged with failing checks, or with none at all — which is + how every merge in this programme happened, mine included.
+Remedy. Require backend-lint-test and
+ frontend-build on main. It costs one setting and makes the eleven
+ ratchets this programme added actually load-bearing.
PR #233 → 0 check-runs on head 25ea94c6db3d, combined state "pending", 0 statuses
+triggers attempted: opened, reopened, synchronize → 0 runs each
+actions/permissions → enabled, allowed_actions: all
+workflow "CI" → state: active
+last CI run anywhere → 2026-08-26T00:30:49Z (now 16:46Z)
+ The configuration is not the cause: on.pull_request parses to
+ None, which is the canonical "all pull requests", and the same file dispatched
+ correctly for feat/frontend-sentry-dsn hours earlier.
admin:org scope this session does not
+ have. Five rounds were spent; a sixth would be guessing.Remedy. Check the organisation's Actions billing. If it is not that, + the evidence above is what a support request needs.
+Nothing was written. These are the rows this audit would file, with the +evidence each already carries.
+ +| Id | Sev | Row |
|---|---|---|
| A-1 | 🟠 High | A public indexable page sells plans the deployment cannot charge for. BILLING_ENABLED set, STRIPE_SECRET_KEY unset, /pricing 200 with robots: index, follow. Checkout degrades honestly to 400 but shows the customer "Stripe is not configured (STRIPE_SECRET_KEY missing)". Same shape as RERANKER_ENABLED; capability_report.py makes three claims and this is not one. Fix: add the claim, then unset the flag or give the route a customer-facing message. |
| A-6 | 🟠 High | main is not protected. #223 made CI run on every PR; nothing makes the result binding, so a PR can merge with failing or absent checks — as every merge in this programme did. A gate that runs and cannot block is a gate an operator believes in. Fix: require backend-lint-test and frontend-build on main. |
| A-7 | 🟡 Medium | No workflow dispatched for 16 hours. PR #233 gets 0 check-runs across opened, reopened and synchronize; Actions is enabled and the workflow active; the same file dispatched correctly hours earlier. Undiagnosed — the fitting hypothesis is an org Actions limit, which needs admin:org to read. Fix: check org Actions billing. |
| A-2 | ⚪ Info | Eleven subsystems have never run on live data, not six. Counted from production's empty tables. For each, a green CI run is the only evidence, and the first real user is the first integration test. Fix: record the exercised/unexercised split so priority arguments start from it; nothing in code. |
| A-3 | ⚪ Info | The mechanical audit is blind on this monorepo. monorepo: true yet manifests read only at the root → empty languages/managers/telemetry, a false telemetry finding, two blind probes. Three probes were blind silently. Fix: belongs upstream in the audit script, not here — recorded so the next run's numbers are not read as measurements. |
| A-5 | ⚪ Info | 149 orphaned code_clusters rows. Verified benign: the read is behind the same flag and a re-enable deletes before inserting. Fix: none required; delete on flag-off if tidiness is wanted. |
Naming these is the point of the section; a report that lists only what it +found reads as coverage it does not have.
+CB-OPS1.--backlog). Open as CB-UX1.F- rows on the existing board were not re-verified
+here. This audit probed production and the seams around yesterday's changes, not the
+standing backlog.firstEvent: none yet. That is "nothing has failed", not "reporting works" — and
+only a real error or a planted test event separates the two.Что готово, что не готово, что недоделано и что ломается в проде. Каждое утверждение + ниже несёт команду, файл со строкой или запрос, которым оно получено. Там, где измерить не удалось, + так и написано — вместо оценки.
+ +Код в очень хорошем состоянии; развёрнутая система — нет. 6 741 бэкенд-тест зелёный, + покрытие 79% против гейта 72%, на доске нет ни одной находки выше Low, дизайн-система соблюдается + почти идеально, секретов в репозитории нет. При этом в проде: индексация репозитория падает 85% + прогонов подряд шестнадцать дней из-за отсутствующего heartbeat, восемь флагов включены + вопреки коду и документации (один из них назван инвариантом vision), трекинга ошибок нет + вообще — Sentry не сконфигурирован, а собственный журнал ошибок содержит три записи и не поймал + ни одного из 143 падений. Продукт задеплоен, но не эксплуатируется: 8 пользователей, 2 проекта, + 0 подписок, 0 GA4-подключений. Разрыв не в качестве кода, а в наблюдаемости и конфигурации прода.
+Одиннадцать срезов, все — на живых артефактах: рабочее дерево, прод-приложение Heroku, +прод-Postgres (только SELECT), OSV API, OpenAPI-схема живого сервиса. Ни одно число ниже не +перенесено из документации — все пересчитаны.
+ +| Срез | Источник истины | Как получено |
|---|---|---|
| Тесты и покрытие | полный прогон, не CI-кэш | pytest tests/ --cov=app · npx vitest run |
| Гейты фронтенда | рабочее дерево | tsc --noEmit · eslint --max-warnings=0 |
| Прод-логи | 1 500 строк, окно 07:42–11:27 UTC | heroku logs -n 1500 |
| Прод-состояние | 66 таблиц Postgres | heroku pg:psql (только SELECT) |
| Прод-конфигурация | 68 переменных окружения | heroku config:get (значения скрыты) |
| Прод-образ | one-off dyno | heroku run python -c "find_spec(...)" |
| API-поверхность | OpenAPI живого сервиса | curl /openapi.json → 209 операций |
| Уязвимости | OSV.dev, 180 пакетов | POST api.osv.dev/v1/querybatch |
| Дефолты флагов | config.py против CLAUDE.md | regex-сверка 20 ключей |
| Доска и сценарии | строки файлов, не сводки | Python-разбор таблиц Markdown |
| Секреты | индекс git | git grep по 5 шаблонам |
Слева — то, что измерено сегодня. Где документация утверждает другое, это отмечено.
+ +CLAUDE.md утверждает «7 009 тестов — 6 326 бэкенд + 683 фронтенд по 93 файлам,
+ покрытие 78%». Измерено сегодня: 6 746 бэкенд (6 741 + 4 skip + 1 xfail) и 709 фронтенд
+ по 94 файлам, покрытие 79%. Цифра помечена «measured 2026-08-20» и просто устарела на три
+ дня — но она подаётся как текущая, а не как снимок.
«Готово» здесь означает: код есть, тесты зелёные, и в проде есть данные, +доказывающие, что подсистема работала. «Не проверено в проде» — код и тесты есть, а прод-данных ноль; +это не обвинение, а точное описание того, чего мы про подсистему не знаем.
+ +| Подсистема | Статус | Доказательство из прода |
|---|---|---|
| Аутентификация (email/пароль, Google, верификация, сброс) | +готово | +users=8 · audit_logs=52 |
| Мультиарендность — проекты, участники, роли, инвайты | +готово | +projects=2 · project_members=6 |
| Чат-агент (REST / SSE / WS, оркестратор, гейты) | +готово | +sessions=22 · messages=116 traces=212 · spans=9 526 |
| Учёт токенов и бюджеты | +готово | +token_usage=5 953 |
| Индексация схемы БД | +готово | +37 completed / 5 failed последнее падение 18.07 |
| Синхронизация код↔БД | +готово | +29 completed / 0 failed · rows=354 |
| MCP-сервер (смонтированный, персональные токены) | +готово | +резолв токена в логах 11:24 сегодня |
| Заголовки безопасности CSP / HSTS / XFO | +готово | +проверено curl -I на живом хосте |
| Восстановление BM25 на диске (F-KNOW-12) | +готово | +rebuilt=1 schema_rebuilt=1 failed=0 |
| Обучение агента (память по подключению) | +готово | +80 записей, все с connection_id |
| Инсайты и trust-скоры | +готово | +insight_records=125 · trust_scores=125 |
| Индексация репозитория (M1–M6) | +ломается | +12 completed / 70 failed 64 из них на шаге graph_build |
| Код-граф | +частично | +25 421 символ, но всего 2 121 ребро ≈0,08 ребра на символ |
| Кластеризация графа | +частично | +code_clusters=147 (значит, доходит иногда) |
| Реранкер (кросс-энкодер) | +no-op | +флаг=true, но sentence_transformers ABSENT |
| Эмбеддер 768-d (BAAI/bge-base) | +no-op | +молча падает на MiniLM 384-d |
| Биллинг Stripe | +не проверено в проде | +billing_enabled=true, но subscriptions=0 · stripe_events=0 |
| Аналитика GA4 | +не проверено в проде | +collect_enabled=true, но vendor_credentials=0 · imports=0 · факты=0 |
| Расписания запросов | +не проверено в проде | +scheduled_queries=0 |
| Расследования данных (InvestigationAgent) | +не проверено в проде | +data_investigations=0 |
| Голосование за обучения | +не проверено в проде | +learning_votes=0 |
| Уведомления | +не проверено в проде | +notifications=0 |
| Наблюдаемость / трекинг ошибок | +отсутствует | +SENTRY_DSN не задан · error_log=3 строки |
Шесть подсистем — биллинг, GA4, расписания, расследования, голосование, уведомления — написаны, + покрыты тестами, включены в проде и ни разу не выполнялись на живых данных. Это не баг: продукт + ещё не в коммерческой эксплуатации. Но это значит, что для них «зелёный CI» — единственное + свидетельство, а первый реальный пользователь окажется первым интеграционным тестом. Про эти шесть + честная формулировка — «не проверено», а не «готово».
+Приложение checkmydata-api, стек container, регион us,
+web + worker по одному Standard-1X дино, Postgres essential-1, Redis mini.
+Релиз v259 от 21 августа 09:03 — и это ровно HEAD ветки main
+(a5f68880, деплой-workflow success), то есть прод не отстаёт от кода.
Индексация репозитория падает каждый день с 7 августа, всегда на одном и том же шаге, +всегда с одной и той же причиной:
+ +| Дата | Падений | Шаг, на котором умерло |
|---|---|---|
| 22.08 | 1 | graph_build |
| 21.08 | 5 | generate_docs, graph_build |
| 20.08 | 4 | generate_docs |
| 19.08 | 5 | graph_build |
| 18.08 | 5 | graph_build |
| 17.08 | 5 | graph_build |
| 16.08 | 1 | graph_build |
| 13.08 → 07.08 | 30 | graph_build (каждый день) |
Итог по всей истории: 64 из 70 падений — на graph_build,
+failure_kind='fatal', error='stale run reaped'. Причина установлена и
+описана ниже как находка N1 — это не исключение в коде графа, а отсутствующий heartbeat.
За окно 3 ч 45 мин: 611 × Error R14 (Memory quota exceeded), 0 × R15 (SIGKILL),
+0 × H12/H13/H10. Значение памяти — плоское mem=899M(150.4%) во всех 611 замерах,
+минимум равен максимуму.
Плоские 899 МБ без роста и без единого R15 означают, что 899 МБ — это базовый след воркера в
+ покое, а не утечка индексации. Воркер превышает квоту, ничего не делая. Фикс размера батча
+ (EMBEDDING_UPSERT_BATCH_SIZE=8) держится — SIGKILL исчез полностью. Но задача
+ #12 формулировала блокер как «частота деплоев»: деплои прекратились 21 августа в 09:03, а падение
+ graph_build случилось и 22-го. Частота деплоев была не при чём. Настоящая
+ причина — N1.
За всё окно: 0 × pipeline_end, 0 × pipeline_start,
+0 × code_symbol_embed, 0 × Traceback, 0 × DuplicateIDError.
+Судить по этому нельзя: окно покрывает 09:42–13:27 по местному, а суточная синхронизация
+запускается в 00:00 и 02:00. Строки skipped=2 в кроне — не баг: оба проекта
+просто ждут своего часа. Heroku хранит 1 500 строк, за пределы окна не заглянуть.
Восстановление BM25 после рестарта (F-KNOW-12) — в логах старта сегодня:
+ bm25_local_reconcile: rebuilt=1 schema_rebuilt=1 present=0 no_docs=1 failed=0.
+ Оба индекса пересобрались на web-дино за 13 секунд. Это фикс из этой серии работ, и он держится.
Заголовки безопасности — CSP с frame-ancestors 'none' и
+ object-src 'none', HSTS max-age=31536000; includeSubDomains,
+ X-Frame-Options: DENY, nosniff,
+ Referrer-Policy: strict-origin-when-cross-origin. Полный образцовый набор.
Тринадцать находок, которых нет на доске. Каждая — с доказательством и с фиксом. +Severity расставлена по достижимости в текущей конфигурации, а не по громкости названия.
+ +run_repo_index нет heartbeat — и 300 секунд превратились в лимит на один шагHIGHИз трёх долгих фоновых функций в worker.py две обёрнуты в
+ heartbeat(...), а третья — нет. heartbeat_at у строки прогона обновляется
+ только на входе в шаг и на выходе из него; внутри шага ничто не тикает. Значит любой шаг,
+ идущий дольше stale_running_heartbeat_timeout_seconds, объявляется мёртвым, пока он
+ работает. graph_build на 25 421 символе идёт дольше — и его добивают. Каждый день.
Это объясняет и кажущееся противоречие: успешные прогоны длились 417 с и 2 568 с и не были + добиты, потому что у них много шагов, и каждый — короче 300 с.
+backend/app/worker.py:221-233 — обёртки нет;
+ сравнить с run_db_index :114-128 и run_code_db_sync
+ :190-201, где она есть · app/services/run_coordinator.py:250-283 —
+ heartbeat_at = _now() на входе и выходе шага · app/config.py:537 —
+ stale_running_heartbeat_timeout_seconds: int = 300current_step='graph_build',
+ error='stale run reaped', ежедневно с 07.08run_repo_index в
+ app.core.heartbeat.heartbeat с тикером на строку indexing_runs, ровно как
+ сделано для двух других. Тест, который должен упасть до фикса: шаг-заглушка длиннее таймаута не
+ должен быть добит reaper'ом.Развёрнутая конфигурация существенно отличается от задокументированной. Особенно важен последний
+ ряд: CLAUDE.md:274 прямо называет его инвариантом vision, а vision.md:73
+ формулирует так — «знание об одной базе никогда не протекает в запросы к другой».
| Флаг | Прод | Дефолт кода | Чем это грозит |
|---|---|---|---|
| CLUSTERING_ENABLED | true | False | +1 CPU-тяжёлый шаг в манифест |
| GIT_POLL_ENABLED | true | False | из семьи «ingestion automation», по правилу дома — off |
| GIT_WEBHOOK_ENABLED | true | False | то же |
| AUTO_SYNC_AFTER_INDEX | true | False | то же |
| FRESHNESS_RECONCILER_ENABLED | true | False | то же |
| SCHEMA_CHANGE_ALERTS_ENABLED | true | False | то же |
| ANALYTICS_COLLECT_ENABLED | true | False | по расписанию зовёт сторонние API |
| DATA_GATE_LLM_SEMANTICS | true | False | лишний LLM-вызов на каждый гейт |
| CROSS_CONNECTION_LEARNINGS_ENABLED | true | False | снимает защиту, названную инвариантом vision |
В данных инвариант пока не нарушен: все 80 строк agent_learnings имеют
+ непустой connection_id, глобальных нет. Выключена именно защита, а не соблюдение.
heroku config:get <KEY> против
+ grep -m1 '^\s*<key>:' backend/app/config.py по 20 ключам ·
+ SELECT (connection_id IS NULL), count(*) FROM agent_learnings GROUP BY 1 → f | 80CLAUDE.md, почему прежнее правило больше не
+ действует. Третий вариант — оставить как есть — допустим только для тех, где расхождение осознанно
+ и записано. CROSS_CONNECTION_LEARNINGS_ENABLED под это исключение не попадает: пока
+ vision.md называет его инвариантом, прод не должен его снимать молча.SENTRY_DSN отсутствует в списке 68 переменных окружения, то есть Sentry не
+ инициализируется вовсе — при том что sentry-sdk[fastapi] в зависимостях,
+ @sentry/nextjs в package.json, а код инициализации со скрабингом PII
+ написан и лежит в app/core/sentry.py. Готовая наблюдаемость просто не подключена.
Собственный журнал продукта error_log содержит три записи, самая свежая от
+ 17 августа. Ни одно из 143 падений фоновых прогонов в него не попало. То есть самая частая
+ производственная ошибка системы не видна ни в одном из двух предназначенных для этого каналов.
heroku config — ключа SENTRY_DSN нет ·
+ SELECT count(*) FROM error_log → 3 ·
+ SELECT status, count(*) FROM indexing_runs GROUP BY status → failed 143«Stale: pipeline_end never received»,
+ 12 повторов, статус open. Журнал поймал проблему чата, но не индексации.SENTRY_DSN — код уже готов; (2) писать в
+ error_log из пути reaper'а: строка, добитая как fatal, — это ровно то
+ событие, для которого журнал существует.RERANKER_ENABLED=true в проде, а в образе нет ни
+ sentence_transformers, ни torch — проверено запуском внутри прода.
+ Реранкер честно деградирует в NoopReranker и один раз пишет в лог, так что аварии нет.
+ Но оператор, читающий heroku config, видит включённую фичу, которой не существует.
Та же форма у эмбеддера: дефолт chroma_embedding_model —
+ BAAI/bge-base-en-v1.5 (768-d), в проде переменная не задана, библиотеки нет, и Chroma
+ молча берёт свой ONNX MiniLM (384-d). Предупреждение при импорте воспроизводится дословно.
heroku run python -c "find_spec(...)" →
+ sentence_transformers → ABSENT, torch → ABSENT,
+ onnxruntime → PRESENTDockerfile.backend:27 — pip install ".[redis]", а
+ sentence-transformers живёт в extra ml (pyproject.toml:59-62)Embedding model BAAI/bge-base-en-v1.5 requires the optional
+ 'sentence-transformers' package, which is not installed; using ChromaDB's built-in default
+ (all-MiniLM-L6-v2, 384-dim)RERANKER_ENABLED с прода (честнее — сейчас он
+ ничего не даёт), либо поставить extra ml вместе с полным переиндексом, потому что
+ векторы 384-d и 768-d несопоставимы. Отдельно стоит сделать так, чтобы флаг, включённый без
+ зависимости, отказывался стартовать или писал предупреждение уровня WARNING при
+ каждом запуске, а не один раз.docs/qa-audit/issues.md на ветке proj/leave-and-caps — той, что несёт
+ открытый PR #218 — содержит три сырых маркера конфликта на строках 91, 97 и 100. Абзац с итогом
+ доски продублирован четыре раза с противоречащими числами. На main маркеров
+ нет, но дубль абзаца уже смержен — там их два.
Отдельно неприятно то, почему это прошло: doc-ratchet проверяет, что фраза «N open rows + and M struck» в файле присутствует, а не что она присутствует однажды. Проверка, + которая ловит отсутствие, но не ловит дублирование, — и есть тот случай, когда зелёный гейт хуже + отсутствующего.
+git grep -nE '^(<<<<<<< |=======$|>>>>>>> )'
+ → docs/qa-audit/issues.md:91,97,100 · вхождений 'not estimated:': ветка
+ 4, main 2, должно быть 1Манифест ставит record_index на позицию 9 и дописывает шесть флаговых шагов на
+ позиции 10–15. Раннер же выполняет record_index последним — после
+ bm25_build. В результате все 12 успешных прогонов в проде стоят на
+ step_index=9, total_steps=15. Процент при этом честный — 100, его выставляет
+ finish(), — так что пользователь видит «100%, шаг 9 из 15» одновременно.
app/knowledge/run_manifests.py:21-30 — порядок манифеста ·
+ app/knowledge/pipeline_runner.py:1779 — record_index против
+ :1254 — bm25_buildSELECT status,current_step,step_index,total_steps,progress_pct,count(*)
+ → completed | record_index | 9 | 15 | 100 | 12record_index, а не после. Тест: step_position каждого шага должен
+ монотонно возрастать в том порядке, в котором раннер их вызывает./docs и /openapi.json открыты в проде без аутентификацииLOWОба отдают HTTP 200 анонимно; схема — 227 139 байт, полный перечень 209 операций и всех моделей.
+ Для FastAPI это поведение по умолчанию, и многие команды сознательно его оставляют. Находка не в
+ том, что это опасно, а в том, что это не выглядит решением: ни в
+ SECURITY.md, ни в docs/DEPLOYMENT.md нет строки, объясняющей выбор.
curl -o /dev/null -w '%{http_code}' https://<prod>/openapi.json → 200,
+ 227 139 байт · /docs → 200FastAPI(docs_url=None, openapi_url=None) под флагом, либо записать решение оставить
+ открытым — с причиной. Молчание здесь и есть дефект.OSV даёт для установленной версии GHSA-f4j7-r4q5-qw2c /
+ PYSEC-2026-311 — «pre-authentication code injection», severity CRITICAL,
+ и исправленной версии не существует.
Почему severity здесь всё-таки Low: CHROMA_SERVER_URL в проде не задан,
+ CHROMA_PERSIST_DIR=/app/data/chroma, и vector_store.py:154-157 берёт
+ HttpClient только при заданном URL — иначе embedded PersistentClient.
+ HTTP-слушателя нет, значит и «pre-auth» вектора нет. Но severity станет Critical в тот момент,
+ когда кто-то настроит удалённую Chroma, и произойдёт это одной переменной окружения.
querybatch; у chromadb==1.5.9
+ 2 advisory, fixed-in: NONE ·
+ Конфигурация: heroku config:get CHROMA_SERVER_URL → пустоCHROMA_SERVER_URL и уязвимой версии chromadb отказываться стартовать или
+ писать CRITICAL в лог. Это тот случай, когда защищать надо не текущую конфигурацию, а
+ переход в следующую.API.mdLOWВ проде 172 пути и 209 операций. API.md называет 130 путей; 49 не совпадают
+ дословно, из них 25 не упомянуты никак — ни путём, ни последним сегментом. Среди них целые
+ группы: пять эндпоинтов обучений по подключению, три управления прогонами
+ (cancel / retry / GET), три журнала ошибок и отказов
+ запросов, /api/chat/search, /api/chat/explain-sql,
+ /api/projects/access-requests.
API.md,
+ нормализация {param}→{id}; вторым проходом отсеяны те, что упомянуты прозой или
+ wildcard'ом вроде /api/auth/*app.openapi() с API.md и падает на новом
+ недокументированном пути. Документация, синхронность которой доказывается кодом возврата,
+ не расходится.В индексе docs/ux/scenarios.md — 127 строк, у всех 127 статус
+ implemented, у 125 вердикт PASS. Даты проверки: 110 из них —
+ 19 июля, то есть 35 дней назад. За это время на main легло 152 коммита, включая
+ изменения пользовательского поведения. Плюс: только 22 из 127 сценариев упоминаются
+ где-либо в коде или тестах — у остальных 105 нет якоря, по которому реализацию можно проверить
+ автоматически.
Формулировка «100% implemented, 98% PASS» здесь — утверждение, а не измерение.
+{'implemented': 127},
+ {'PASS': 125, 'other': 2}, даты {'2026-07-19': 110, '2026-08-16': 5,
+ '2026-08-19': 9, '2026-08-20': 1, '2026-08-21': 2} · перекрёстная сверка
+ SCN-\d+ по всем .py/.ts/.tsx → 22 из 127/ux-audit партиями и ставить якоря
+ SCN-NNN в тесты, начиная со сценариев, которых коснулись последние 152 коммита.[Unreleased]INFOpyproject.toml и package.json согласованно говорят 1.16.0, в
+ CHANGELOG.md есть датированная секция [1.16.0] — но тега
+ v1.16.0 не существует. Последний тег — v1.15.1 от 9 июля,
+ с тех пор на main 152 коммита, а в [Unreleased] накопилось
+ 175 пунктов на 1 176 строк.
Причина понятна и сама по себе не порочна: авто-деплой на Heroku по мержу расцепил «деплой» и + «релиз». Но версия в файлах теперь не соответствует ни одному тегу, и «какой код в v259» отвечается + только по SHA.
+git tag --sort=-creatordate → v1.15.1
+ первый · git rev-parse -q --verify refs/tags/v1.16.0 → пусто ·
+ git rev-list --count v1.15.1..origin/main → 152 · пунктов
+ '^- ' в [Unreleased] → 175В полном прогоне (94 файла) падают 2 теста из 709 — GA4ConnectionForm.test.tsx,
+ оба по Test timed out in 5000ms. Тот же файл в одиночку: 10 passed.
+ То есть тест не сломан, он не успевает при параллельной нагрузке. CI зелёный — значит там либо
+ быстрее, либо повезло, и это ровно тот класс дефекта, который проявляется у кого-то другого.
Test Files 2 failed | 92 passed (94),
+ Tests 2 failed | 707 passed (709) ·
+ Одиночный: npx vitest run src/__tests__/components/GA4ConnectionForm.test.tsx
+ → 1 passed (1) · 10 passed (10)testTimeout для этого файла или убрать из него
+ ожидание, зависящее от планировщика. Флак в CI дороже, чем кажется: он учит игнорировать красное.Строки CB-M5 и CB-L1 фиксируют конкретные числа. Пересчёт сегодня
+ даёт больше по всем четырём метрикам — сильнее всего по except Exception, +18%.
| Метрика | На доске | Сегодня | Δ |
|---|---|---|---|
| except Exception | 516 | 611 | +95 |
| except …: pass | 51 | 54 | +3 |
| # type: ignore | 47 | 49 | +2 |
| # noqa | 104 | 128 | +24 |
F-REPO-04 — ProjectRepository.auth_token_encrypted: колонка объявлена в
+модели, принимается параметром в RepositoryService.create, не передаётся ни одним
+вызывающим, отсутствует в ALLOWED_UPDATE_FIELDS и не читается нигде. Это либо
+незаконченная HTTPS-аутентификация репозитория, либо балласт. Колонка, предназначенная хранить
+секреты, не должна сидеть в схеме без объяснения — но выбор между «дописать» и «снять миграцией»
+продуктовый, не мой.
F-LEARN-04 и F-LEARN-05 — семантические противоречия между обучениями и
+дедупликация перефразировок. Обе требуют эмбеддингов или LLM-вызова там, где сейчас нет ни того, ни
+другого. Приклеить их к обычной правке значило бы спрятать стоимость: это выбор архитектуры и бюджета
+токенов, а не строчка кода.
| PR | Что несёт | База | CI | Блокер |
|---|---|---|---|---|
| #217 | домен роли: F-PROJ-07/08/11 | main | +SUCCESS ×2 | готов к мержу |
| #218 | выход из проекта + ограниченные списки: F-PROJ-12/13 | +proj/role-domain | нет вердикта | +N5 — конфликт-маркеры в файле; мержить после #217 |
Стек на #217 сделан осознанно: #218 нужны logger и VALID_ROLES из него.
+Порядок — сначала #217, затем #218 перецелится на main автоматически. Но #218 нельзя мержить как
+есть: находка N5 — в его собственном диффе.
Покрытие 79% в целом, но распределено неровно, и наименее покрытые модули — это точка входа +продукта и тот самый пайплайн, который падает в проде.
+| Модуль | Покрытие | Не покрыто строк | Почему это важно |
|---|---|---|---|
| app/api/routes/chat.py | 35% | 493 | главная точка входа продукта |
| app/main.py | 40% | 492 | старт, кроны, lifespan |
| app/knowledge/pipeline_runner.py | 53% | 326 | падает в проде каждый день |
| app/api/routes/repos.py | 50% | 229 | запуск индексации |
| app/knowledge/code_db_sync_pipeline.py | 56% | 208 | код↔БД |
| app/api/routes/chat_utility.py | 31% | 192 | explain-sql, estimate, summarize |
| app/connectors/ssh_exec.py | 47% | 158 | только что переработан (F-SSH-07) |
| app/api/routes/feed.py | 13% | 135 | самое низкое покрытие в проекте |
Файлов с нулевым покрытием — ноль из 380. Это хороший знак: непокрытое здесь — это ветки, +а не забытые модули.
+ +Числа пересчитаны из строк файла, не взяты из его сводки — и они не совпали с +тем, что я сам сообщал в предыдущем отчёте.
+ +| открыто | 42 (39 F- + 3 CB-) |
| закрыто | 70 |
| Critical / High / Medium | 0 / 0 / 0 |
| Low / Info | 30 / 12 |
| открыто | 37 (34 F- + 3 CB-) |
| закрыто | 75 |
| Critical / High / Medium | 0 / 0 / 0 |
| Low / Info | 25 / 12 |
В отчёте от 21 августа я написал «осталось 39 (27 Low, 12 Info)». Правильно: 42 открытых
+ строки — 30 Low и 12 Info. Ошибка возникла так: grep -oE по emoji в этой локали
+ отдаёт неверный результат — на 42 строках он вернул «42 🟢» при том, что в файле явно есть строки с
+ ⚪. Пересчёт Python'ом по позиции ячейки даёт 30/12. Числа выше и в остальном отчёте получены
+ вторым способом.
Тринадцать находок из этой серии работ оказались неверны как написаны — и каждый раз это +вскрывалось одинаково: тест, написанный чтобы упасть, проходил. Из тринадцати разобранных подробно +восемь пришлось переформулировать инверсиями — «200 вместо 500», «сигнал неразличим» вместо «сигнала +нет», «всегда» вместо «после рестарта», гонка, которой не существует. Это единственная процедура из +той серии, которую стоит закрепить правилом: находка не считается подтверждённой, пока тест, +написанный чтобы упасть, действительно не упал.
+ +Сверка двадцати дефолтов флагов из CLAUDE.md с config.py дала
+ полное совпадение — включая тонкие места вроде reranker_enabled=False (после
+ исправления 10 августа), clustering_enabled=False,
+ embedding_upsert_batch_size=8. Документация здесь не дрейфует, и это редкость на
+ 100 тысячах строк кода.
CHANGELOG.md на 3 468 строк, ADR на месте, у каждой находки на доске — id, severity
+ и путь. Инженерная гигиена документов заметно выше среднего.
Расхождения, которые нашлись, собраны в находках N5 (маркеры конфликта и дубли абзаца), +N9 (25 эндпоинтов), N10 (устаревшие верификации сценариев) и в поправке к числу тестов +выше. Ни одно из них не про неверное описание архитектуры — все про числа и списки, которые никто +не пересчитывает. Это ровно тот класс, который лечится ratchet-тестом, а не вычиткой.
+ +Это важно и легко перепутать. Локальный venv несёт 32 advisory в 6 пакетах, из них 17
+ исправимы обновлением. Но Dockerfile ставит зависимости по нижним границам, поэтому
+ прод, собранный 21 августа, получил уже исправленные версии: aiohttp 3.14.3
+ (fixed), cryptography 50.0.0 (fixed), GitPython 3.1.59 (fixed).
+ То есть 17 advisory, которые видит локальный аудит, прода не касаются — их закрыли до сборки.
+ Устарел локальный venv, а не проект.
| Пакет | Локально | В проде | Что реально висит на проде |
|---|---|---|---|
| aiohttp | 3.14.1 | 3.14.3 | закрыто 6 advisory исправлены |
| cryptography | 49.0.0 | 50.0.0 | закрыто 2 advisory исправлены |
| GitPython | 3.1.55 | 3.1.59 | закрыто 9 advisory исправлены |
| chromadb | 1.5.9 | 1.5.9 | висит CRITICAL, фикса нет — см. N8 |
| ecdsa | 0.19.2 | 0.19.2 | висит HIGH, фикса нет — но не на пути авторизации |
Про ecdsa честно: GHSA-wj6h-64fc-37mp — тайминг-атака Minerva на P-256,
+исправленной версии нет. Пакет приходит транзитивно через python-jose, но
+jwt_algorithm = "HS256" (config.py:148) — алгоритм симметричный, ECDSA
+на P-256 в аутентификации не участвует. Severity в этой конфигурации — низкая, и это вывод из
+конфигурации, а не из желания.
npm audit — 0 critical, 7 high, 0 moderate/low. Среди них сам
+next, а также sharp (наследует четыре CVE libvips),
+postcss (XSS через неэкранированный </style>),
+js-yaml, brace-expansion, fast-uri, nanoid.
+Часть — только сборочные, но next и sharp — рантайм.
sk-…, AKIA…, PEM-заголовки,
+ ghp_…, lin_api_…) дал только placeholder-строки в UI
+ (SshKeyManager.tsx, vendor-credentials.ts). Отслеживаемых
+ .env — только два .example. Присваиваний
+ SECRET=/TOKEN= с высокой энтропией — ноль.bg-slate-500 и подобных) во всём frontend/src — 2.
+ console.log в продакшн-коде — 0. Использований типа any — 1.
+ @ts-ignore — 0; шесть eslint-disable-next-line, все точечные и
+ обоснованные. tsc --noEmit и eslint --max-warnings=0 — оба чистые.Шесть раз за этот разбор моя собственная команда соврала — и каждый раз в сторону +более уверенного вывода. Записываю их, потому что вывод, полученный сломанным измерением, выглядит +точно так же, как правильный.
+ +| Что я сделал | Что получил | Как поймал |
|---|---|---|
npx vitest run … | tail -40 |
+ VITEST_EXIT=0 при двух упавших тестах — код возврата принадлежал tail |
+ увидел 2 failed в тексте, который сам же напечатал |
grep -oE '🔴|🟠|🟡|🟢|⚪' |
+ «42 🟢» при том, что в файле явно есть ⚪ | +прочитал строки глазами; пересчитал Python'ом по позиции ячейки |
uvx pip-audit без аргументов |
+ «No known vulnerabilities found» — проверена среда самого uvx, не проект | +0 пакетов вместо 180; перешёл на OSV API по списку из importlib.metadata |
regex по @router.… для путей API |
+ «159 недокументированных» — префиксы роутеров не захватывались | +пути вышли как /ask вместо /api/chat/ask; взял OpenAPI живого прода |
regex (GET|POST) /api/ по API.md |
+ «19 упоминаний из 208» — документ использует табличный формат | +цифра была слишком плохой, чтобы быть правдой; пересчёт дал 130 из 172 |
вывод «шаги 10–15 пропущены» из step 9/15 |
+ серьёзное обвинение пайплайну в молчаливом частичном успехе | +проверил порядок вызовов в раннере: record_index — последний. Осталась находка
+ N6, но она Low, а не High |
Пять из шести — это сломанное измерение, выдающее правдоподобный результат. Ни одно не
+ выглядело как ошибка: exit=0, «уязвимостей нет», «42 строки» — всё это нормальные
+ ответы. Единственное, что их вскрыло, — сверка второго измерения с первым и чтение исходных строк
+ глазами. Отсюда практическое правило: число, на котором держится вывод, должно быть получено
+ дважды разными путями — особенно когда первое измерение подтверждает то, что ты и так думал.
Порядок — по отношению «стоимость исправления к тому, что оно перестанет скрывать», +а не по severity.
+ +| # | Действие | Почему первым | Объём |
|---|---|---|---|
| 1 | Задать SENTRY_DSN в проде |
+ код инициализации со скрабингом PII уже написан. Пока трекинга нет, каждая следующая находка + будет добываться так же вручную, как эта | одна переменная |
| 2 | Обернуть run_repo_index в heartbeat (N1) |
+ снимает 85% отказов индексации, которые идут 16 дней. Паттерн уже есть в том же файле для + двух других функций | ~10 строк + тест |
| 3 | Вычистить конфликт-маркеры и мержить #217 → #218 (N5) | +#218 нельзя мержить как есть; #217 зелёный и ждёт | минуты |
| 4 | Писать в error_log из пути reaper'а (N3) |
+ журнал ошибок продукта не поймал ни одного из 143 падений — он существует ровно для этого | +небольшой |
| 5 | Решить по девяти флагам прода (N2) | +сначала CROSS_CONNECTION_LEARNINGS_ENABLED: пока vision называет его инвариантом,
+ прод не должен его снимать молча | решение + запись |
| 6 | Снять RERANKER_ENABLED или поставить extra ml (N4) |
+ конфигурация не должна утверждать возможность, которой нет в образе | +переменная, либо образ + переиндекс |
| 7 | Ratchet-тесты на числа: конфликт-маркеры, дубль абзаца, покрытие API.md, + долг подавлений (N5, N9, N13) | +все четыре расхождения этого разбора — числа, которые никто не пересчитывает. Лечится + тестом, а не вычиткой | средний |
| 8 | Поднять покрытие chat.py (35%) и
+ pipeline_runner.py (53%) |
+ точка входа продукта и модуль, падающий в проде, — наименее покрытые в проекте | +крупный |
| 9 | Разгрузить воркер: 899 МБ при квоте 512 МБ в покое | +R15 ушли, но 611 × R14 за 4 часа — это постоянный свап. Resize или вынос индексации | +инфраструктурный |
| 10 | Прогнать /ux-audit по сценариям, которых коснулись
+ 152 коммита (N10) |
+ «100% implemented / PASS» — утверждение пятинедельной давности, а не измерение | +крупный |
| 11 | Решить судьбу auth_token_encrypted (F-REPO-04) |
+ колонка под секреты без писателя и читателя. Выбор «дописать или снять» — продуктовый | +решение |
| 12 | Разобраться с 7 high в npm, начиная с next и sharp |
+ единственные два из семи, что попадают в рантайм | средний |
+Отчёт собран 23 августа 2026 из рабочего дерева proj/leave-and-caps,
+origin/main @ a5f6888 и живого прода checkmydata-api v259.
+Прод-Postgres читался только запросами SELECT; персональные данные не извлекались —
+только агрегаты. Значения переменных окружения нигде не печатались; для SENTRY_DSN
+проверялось лишь наличие.
+Всё, что не удалось измерить, названо в тексте неизмеренным. Шесть сломанных измерений,
+встретившихся по ходу, перечислены в разделе 10 вместе с тем, как каждое было поймано.
+
checkmydata-ai · 21 августа 2026 · цикл остановлен по запросу · ветка feat/ledger-redesign → 26 PR в main
За сессию закрыто 75 находок из аудита, смержено 26 PR, два PR открыты и ждут мержа. Критических, высоких и средних находок на доске не осталось. Ниже — что сделано, что нет, и что выяснилось про сам аудит.
+ +mainИз тринадцати разобранных подробно находок восемь оказались неверны как написаны — и не в мелочах, а инверсиями. Это не упрёк автору доски: так выглядит любой аудит, написанный по чтению кода, а не по его исполнению.
+ +| Строка | Что было написано | Что оказалось |
|---|---|---|
| F-PROJ-06 | «partial-success 500s» | 500 не бывает вовсе: 200, скрывающая провал — письмо не ушло, ответ успешный |
| F-VIZ-01 | «нет признака свежести» | Признак есть. Он не различает «никто не открывал» и «обещанное обновление сломано» |
| F-KNOW-07 | «добавить метрику промаха» | Метрика была — и срабатывала на здоровом пути, поэтому не значила ничего |
| F-KNOW-12 | «dense-only после рестарта» | Всегда: писатель в воркере, читатель в веб-дино, разные диски |
| F-SSH-04 | «db_port не экранирован» | Верно про код, но недостижимо — тип закрывает все пути |
| F-SSH-05 | «гонка check-then-pin» | Гонки нет: между проверкой и записью ноль await |
| F-GIT-02 | «наследует риск RCE» | Не наследует: проверка стоит в вызываемом, её наследуют все |
| F-PROJ-07 | «не валидирует строку роли» | Хуже: fail-open — опечатка в требуемой роли открывала эндпоинт всем |
Каждый раз это выяснялось одним и тем же способом: тест, написанный чтобы упасть, проходил. Это дешевле любого чтения кода, и это единственная процедура из сессии, которую стоит закрепить как правило.+ +
Больше половины закрытых находок — один и тот же дефект в разных местах: сигнал, который не может сказать, какая из двух вещей произошла.
+ +| Находка | Сигнал существовал | Два состояния, которые он сливал |
|---|---|---|
| F-PROJ-06 | _send логировал сбой | «отправлено» против «залогировано и всё равно 200» |
| F-VIZ-01 | timeAgo(last_executed_at) | «никто не открывал» против «обновление сломано» |
| F-KNOW-07 | retrieval_degraded_total | «лексически не совпало» против «индекса нет на этой машине» |
| F-DG-08 | кросс-стадийная проверка | «пропорции в норме» против «оба числа обрезаны» |
| F-SCHED-03 | предикат жнеца | «свежая» против «возраст неизвестен» |
| F-PROJ-13 | список участников | «вся команда» против «первые 500 из 5000» |
Опознаётся по двум приметам: метка причины с единственным значением, и булево там, где у явления три состояния.
+ +mainНи одной Critical, High или Medium. Из 39: 27 Low и 12 Info. Две из них я завёл сам в этой сессии, намеренно не закрывая.
+ +ProjectRepository.auth_token_encrypted: зашифрованная колонка, которую никто не пишет и никто не читает. Либо недоделанная HTTPS-аутентификация, либо мёртвый вес. Это продуктовое решение, не техническое — доделать фичу или снять колонку миграцией.F-LLM-05 (мутация под правами viewer — её независимо подтвердила развёртка по 143 точкам вызова), F-BILL-08 (чарджбэк не отзывает доступ), F-CONN-07 (исчерпание удалённых соединений), F-CHAT-04 (устаревший конфиг в WS-сессии), F-CHAT-08 (fatal-стадия не отменяет соседние), F-SCHED-06 (нет минимального интервала расписаний), F-FE-03 (a11y и токены не проаудированы) и далее.
+ +| PR | Что | Состояние |
|---|---|---|
| #217 | F-PROJ-07/08/11 — домен роли: UnknownRoleError, AssignableRole, защита upsert от гонки | перебазирован, ждёт CI |
| #218 | F-PROJ-12/13 — выход из проекта, ограниченные списки с честной меткой обрезания | в стеке на #217 |
#218 стоит на #217 осознанно: ему нужны logger и VALID_ROLES оттуда. Порядок мержа — сначала #217, затем #218 сам перенацелится на main.
pipeline_end.
+Формулировка исправлена по измерению, а не по памяти: за реальное окно 3.5 часа 0× Error R15 (фикс размера батча держится, никого не убивают), но 204× R14 и pipeline_end по-прежнему ноль. Стадия code_symbol_embed стартует на 25 270 символах и через 23 минуты воркер перезапускается посреди неё — без убийства и без ошибки. Все пять перезапусков в окне совпали с релизами v240–v244, каждый через 12–14 с после релиза.
Блокирует частота моих же деплоев, а не память. Прогону нужно больше времени, чем интервал между мержами, и цикл его не давал. Теперь цикл остановлен — окно появится само. Resize воркера (heroku ps:resize worker=standard-2x) остаётся правильным долгим решением, но гейтом он не является.
tail -1 — не вердикт. CI поймал F841, который я не увидел, потому что ruff check печатает подсказку под замечанием. Дальше все гейты читались по коду возврата. Тот же класс, что | head, обрезавший мою же развёртку раньше.timeout на macOS нет. Обёрнутый в него heroku logs вернул пустой файл, и каждый grep -c честно сообщил ноль. Ноль от команды, которая не запускалась, неотличим от измеренного нуля..replace(x, y, 1) бьёт в первое вхождение. Три подсадки выглядели непойманными, пока не выяснилось, что они правили не то место. Дважды это было await refresh(), один раз — if role == "owner" в другой функции.Три раза существующий тест защищал находку, а не пропускал её:
+test_behind_emits_pull_warning утверждал наличие слова "pull" — и потому держал совет, который не мог помочь: клон потерял коммиты, а pull не возвращает перезаписанную историю.+0.1 — ровно тот инкремент, который позволял накачать уверенность до 1.0 повторной отправкой одного урока.Утверждать слово — не значит утверждать смысл. Утверждать значение — не значит утверждать, что значение верно.+ +
docs/qa-audit/issues.md. Все числа на ней выводятся из строк, а не набираются: за сессию оба случая расхождения были у чисел, набранных руками. Рэтчет test_the_tally_matches_the_rows_it_summarises держит это.docs/audits/2026-08-19-full-stack-audit.md, разделы 28–31.CHANGELOG.md, раздел [Unreleased].docs/ux/scenarios.md — добавлены SCN-126 (передача владения) и SCN-127 (выход из проекта).