Дата: 2026-08-07 · Ветка: master · База: cc5ce34
Охват: Api (18 .NET-проектов), Web (Electron/TS + ui/*.js), Site, плюс текущий незакоммиченный дифф.
Статус сборки на момент ревью:
npx tsc --noEmit— чисто.npm test— красный:tests/cache-ui-assets.test.ts:41падает, потому что правка вui/pdf-viewer.jsизменила строку исходника, которую тест сравнивает буквально. Это не случайность, а прямое следствие проблемы №4.
Статус на 2026-08-11. Исходный текст ревью ниже сохранён без переписывания: номера и формулировки описывают состояние на базе cc5ce34, а этот раздел — результат последующей работы в незакоммиченном рабочем дереве.
| # | Статус | Результат |
|---|---|---|
| 1 | Выполнено в согласованном объёме | Удалены старая Angular/ASP.NET-оболочка, Middleware, Persistence, их runtime-регистрации и относящиеся к ним тесты старого UI. |
| 2 | Не выполнено как архитектурный рефакторинг | Провайдеры не схлопывались, startCacheUi не разбивался на роутер и файлы маршрутов. |
| 3 | Частично, точечно | В модули вынесены только изолированные части, для которых уже была практическая причина. Массовой миграции ui/*.js нет. |
| 4 | Частично, по затронутой функциональности | Исправлены мешавшие работе source-scraping тесты и добавлены поведенческие проверки. Остальные такие тесты намеренно не переписывались массово. |
| 5 | Выполнено | Bridge вынесен в отдельный файл, протокол поиска структурирован, входящие сообщения ограничены источником. |
| 6 | Не выполнено, сознательно оставлено | Текущий механизм SQLite-миграций не заменялся на PRAGMA user_version. |
| 7 | Выполнено | Чтение метаданных и тяжёлых тел страницы разделено на отдельные запросы. |
| 8 | Выполнено частично | 25 копий проверки заменены одним общим guard; политика отсутствующего Origin сохранена. |
| 9 | Выполнено | Ошибочно закоммиченные Windows cache DB удалены, *.db добавлен в ignore. |
| 10 | Частично выполнено | Нормализация API URL, документация staging и ignore для DB исправлены; Site/ и CSP не трогались. |
| — | Выполнено | Три отдельно описанных ниже дефекта PDF/теста исправлены. |
Сделано:
- удалён
Api/Application/ClientAppсо старой Angular-оболочкой; - удалены
AnalysisSessionsController,NavigationProvidersControllerи проектCommandsHandler; - из
Application.csprojубраны SPA/MSBuild-таргеты, NSwag codegen-таргет и пакеты AutoMapper, ElectronNET, MediatR, SpaServices и NewtonsoftJson, которые обслуживали старую оболочку; - из
Startupубраны SPA static files/proxy, старый Electron bootstrap, MediatR/AutoMapper wiring и синхронныйInitDatabaseAsyncпри старте; - удалены устаревшие
electron.manifest.json,nswag.jsonи статическая копия OpenAPI specification; - удалены проекты
Api/MiddlewareиApi/Persistence, ссылки на них из solution,Applicationи тестов; - удалены DB/analyzer/navigation-тесты старого UI и их
DbTestsBase, но сохранён 101 актуальный тест парсинга, HTML и OneNote document provider; - из
ServicesModuleубраны DB-зависимые регистрации analyzer/save-processing, а изOneNoteModule— регистрация старого navigation provider; - удалён неиспользуемый
NSwagSchemaNameGeneratorс остаточными ссылками на namespaceBibleNote.Middleware; - из launch settings удалён профиль
Electron.NET App, из-за которого обычныйdotnet runпытался запустить отсутствующийelectronize; - README больше не требует
-p:GenerateCode=Falseдля обычной .NET-сборки.
Сначала Middleware/Persistence были оставлены из-за прямых ссылок старого Api/Tests. После уточнения, что эти компоненты обслуживали прежний UI, тесты были разделены по назначению: тесты актуального парсинга сохранены и отвязаны от MediatR/EF, а тесты удалённой DB/analyzer/navigation-ветки удалены вместе с ней. Api/Infrastructure не входит в это удаление: он не имеет потребителей, но его окончательная судьба требует отдельного явного решения.
Предложенное объединение .NET-провайдеров и полное разбиение startCacheUi на router/middleware/routes не выполнялись. Это широкий рефакторинг с большим диффом и высоким риском незаметно изменить маршрутизацию, состояние синхронизации или packaging. Отдельные локальные проблемы из этого блока исправлялись без предварительного архитектурного переворота: общий origin guard и специализированные запросы к кэшу описаны в пунктах 7 и 8.
Массового перевода классических скриптов из index.html на ES-модули не было. Точечно сделано следующее:
- чистая логика сопоставления PDF-ссылок вынесена в
Web/ui/pdf-link-matching.jsи импортируется как обычный модуль; - iframe bridge вынесен в
Web/ui/page-frame-bridge.js; - тесты PDF-сопоставления вызывают экспортированные функции на входных данных вместо вырезания их текста;
- проверки строкового тела bridge удалены или заменены браузерными сценариями поиска,
Ctrl+F, навигации и очистки подсветки; - фиксированное ожидание
waitForTimeout(1200)в PDF E2E убрано в пользу ожидания наблюдаемого состояния.
Остальные source-scraping тесты сохранены. Принято правило репозитория: не переписывать их массово и не переводить UI на ES-модули только ради тестируемости; заменять конкретный тест поведенческим при изменении соответствующей функции или когда он блокирует полезный рефакторинг.
Bridge больше не собирается массивом экранированных строк внутри api-page-view.js. Он поставляется как отдельный UI asset page-frame-bridge.js и подключается в srcdoc внешним script.src.
Дополнительно:
- HTML-frame получает структурированный запрос
{ query, mode, caseSensitive, terms }, а не произвольные regexsource/flags; - regex строится внутри frame с экранированием пользовательского текста; raw regex разрешён только для явного режима
regex, набор флагов формируется кодом; - обработчик сообщений принимает команды только при
event.source === parent; event.originздесь не используется как граница доверия: sandboxedsrcdocбезallow-same-originимеет opaque origin (null), поэтому корректной проверкой является идентичность окна-родителя;- добавлен E2E-сценарий, в котором соседний iframe пытается отправить управляющее сообщение и не влияет на целевой frame.
PDF-протокол поиска оставлен прежним, чтобы не смешивать этот рефакторинг с изменением PDF viewer.
Переход с проверок колонок на массив миграций и PRAGMA user_version не выполнялся по явному решению оставить текущий механизм. Причины: изменение затрагивает открытие всех существующих пользовательских БД, требует проектирования baseline для уже частично мигрировавших схем и политики downgrade. Польза сейчас в основном диагностическая, а цена ошибки — невозможность открыть или корректно обновить локальный кэш.
Возвращаться к этому пункту стоит только как к отдельной задаче с копиями реальных старых БД, тестами обновления с каждой поддерживаемой схемы и заранее определённым поведением при запуске старой версии приложения на новой БД.
Универсальный SELECT * FROM pages WHERE id = ? заменён специализированными функциями:
getCachedPageMetadata— метаданные безcontent_htmlиcontent_text;getCachedPageContent— запрошенное тело (html,textили оба);getCachedPageStatusиgetCachedPageSyncState— узкие статусы.
Вызовы в cache-ui.ts переведены на минимально необходимую форму. Добавлены тесты, проверяющие, что метаданные и статус можно читать без загрузки обоих тяжёлых тел и что обновление одних метаданных не переписывает FTS.
Повторяющиеся проверки удалены из отдельных обработчиков. isCrossOriginStateChangingApiRequest вызывается один раз до маршрутизации и по умолчанию покрывает POST, PUT, PATCH и DELETE под /api/. Добавлен отдельный тест на мутирующие методы, GET, не-API пути и fallback-порт.
Семантика намеренно сохранена совместимой: запрос без Origin допускается, запрос с чужим Origin отклоняется. Требование заголовка для всех запросов сломало бы локальные non-browser вызовы (в частности интеграционные/Ribbon сценарии). Таким образом, проблема дублирования и риск забыть guard в новом мутирующем API закрыты, но это не строгая политика «каждый запрос обязан иметь Origin».
Четыре файла из Web/%SystemDrive%/ProgramData/Microsoft/Windows/Caches удалены из рабочего дерева, после чего удалён и весь оставшийся пустой каркас Web/%SystemDrive%. В корневой и Web .gitignore добавлен *.db, чтобы такой класс cache-файлов не попадал в Git повторно. Конкретный исторический процесс, создавший путь с буквальным %SystemDrive%, не найден; активных ссылок на этот путь в текущих скриптах не обнаружено.
bibleParseConfigFromEnvтеперь нормализуетapiUrlодин раз после применения overrides; повторные.replace(/\/+$/, '')в gateway удалены;- в
Web/README.mdописано, что release-команды сами выполняют stagingvendor/BibleNote,vendor/nodeи Ribbon перед сборкой; *.dbдобавлен в корневой и Web ignore;Site/остаётся untracked и вSite/_headersпо-прежнему нет CSP. Это не исправлялось, потому что не установлено, является ли каталог исходником реального deployment target или локальным результатом публикации. Добавление CSP без проверки сайта также может заблокировать используемые им inline/resource сценарии.
- Сопоставление PDF occurrence:
occurrenceIndexтеперь задаёт приоритетного кандидата, после которого остаётся fallback на остальные свободные совпадения. Чистая логика находится вpdf-link-matching.jsи покрыта прямыми тестами. - Фильтр
pageNumber:null/undefinedявно означают «страница не задана», а заданное значение сравнивается с текущей страницей без преобразованияnullв0. - Красный source assertion в
cache-ui-assets.test.tsудалён/заменён проверкой поведения. После соответствующей серии правок unit-набор снова был зелёным.
После серии изменений, относящихся к этому ревью, выполнялись:
npm.cmd run build— успешно;npm.cmd test— 188/188;npm.cmd run e2e:search— 5/5;node --check Web/ui/page-frame-bridge.js— успешно;- сборка .NET-приложения и HTTP smoke для health/specification — успешно;
- после удаления
Middleware/Persistence:dotnet build BibleNote.sln— 0 ошибок,dotnet test Tests/Tests.csproj --no-build— 101/101, HTTP health —ok, OpenAPI — 13 маршрутов; git diff --check— без ошибок (только предупреждения Git о будущей нормализации LF/CRLF).
Это результаты на момент выполнения соответствующих правок, а не утверждение о состоянии любых более поздних незакоммиченных изменений. Версия приложения не повышалась, installer/portable/release artifact не собирались. Изменения из этого раздела на момент записи истории не закоммичены.
| # | Проблема | Область | Приоритет |
|---|---|---|---|
| 1 | Мёртвая параллельная ветка: .NET + Angular + CQRS + EF | Api |
Высокий |
| 2 | Инверсия гранулярности между стеками | Api, Web/src |
Высокий |
| 3 | ui/*.js — полмегабайта в одном глобальном скоупе |
Web/ui |
Высокий |
| 4 | Тесты проверяют текст исходников, а не поведение | Web/tests |
Высокий |
| 5 | Bridge-скрипт как массив строковых литералов | Web/ui |
Средний |
| 6 | Схема БД: «проверь колонку → ALTER TABLE» вместо версий | Web/src |
Средний |
| 7 | SELECT * по таблице с HTML-телами |
Web/src |
Средний |
| 8 | CSRF-проверка: 25 копий и fail-open | Web/src |
Средний |
| 9 | Мусор в репозитории | репозиторий | Низкий |
| 10 | Мелочи | разное | Низкий |
| — | Баги в текущем незакоммиченном изменении | Web/ui, Web/tests |
Высокий |
Api/Application до сих пор несёт полноценную вторую оболочку продукта, которой никто не пользуется.
Api/Application/Startup.cs:41поднимаетAddSpaStaticFiles,UseSpa, прокси наhttp://localhost:4200, ElectronNET-окно.Api/Application/Application.csproj:41: таргетPublishRunWebpackна каждомdotnet publishделаетnpm install --legacy-peer-depsи сборку Angular вClientApp/(20.ts-файлов, Angular 22, TypeScript 6). Debug-сборка черезDebugEnsureNodeEnvтоже дёргаетnpm install.- Реальный клиент (
Web/src/biblenote-gateway.ts) обращается только кVerseParsingиOneNoteIntegration. ЗначитAnalysisSessionsController,NavigationProvidersController, весьMiddleware(10 MediatR-хендлеров),Persistence(EF + миграции) иCommandsHandler— недостижимы из продукта. - При этом
Api/Application/Startup.cs:117на каждом старте выполняетdbContext.InitDatabaseAsync().GetAwaiter().GetResult()— синхронно блокирует старт ради БД, которую никто не читает.
Отсюда же MediatR 9.0.0 и FluentValidation 9.2.2 (2020 год) в проекте на .NET 10, и NSwagExe_Core31 в таргете NSwag 14.
Что сделать: удалить ClientApp, оба контроллера, Middleware, Persistence, CommandsHandler, пакеты MediatR / FluentValidation / AutoMapper / SpaServices / ElectronNET и оба MSBuild-таргета SPA. Останется Application (2 контроллера) → Services → Providers → Common/Domain. Это убирает ~6 сборок, npm-зависимость из .NET-сборки и необходимость флага -p:GenerateCode=False из README.
Если удалять пока рано — минимум вынести SPA-таргеты под Condition="'$(BuildSpa)'=='true'" и убрать InitDatabaseAsync из горячего старта.
.NET переразбит: 18 сборок на ~15k рукописных строк — по проекту на каждый провайдер (Web.DocumentId, FileSystem.DocumentId, Html, Word, Pdf, …), при том что все они ссылаются на один Services. Дробление ради дробления: границы сборок не отражают ни независимость релизов, ни замену реализаций.
Web недоразбит: Web/src/cache-ui.ts — 2271 строка, из которых 533–2271 это одна функция startCacheUi с 64 проверками маршрута в линейной if-цепочке внутри одного замыкания. Плюс cache-search.ts (50KB), sync.ts (59KB), electron-main.ts (43KB). У cache-ui.ts 17 прямых внутренних импортов — это хаб без слоёв.
Что сделать:
-
.NET: схлопнуть провайдеры в один
BibleNote.Providersс папками. Держать отдельными только те, у кого действительно чужая нативная зависимость (OneNote— COM-interop,Word/Pdf— тяжёлые пакеты). -
Web: заменить
if-цепочку таблицей маршрутов и разнести по файлам:src/http/router.ts — сопоставление method+pattern → handler src/http/middleware.ts — origin-guard, json-body, error→status, логирование src/http/routes/sync.ts — ~8 маршрутов src/http/routes/inductive.ts src/http/routes/bible.ts src/http/routes/onenote.ts src/http/routes/system.tsСостояние (
syncState,microsoftLoginState,SingleFlight) — в явный объект контекста, передаваемый хендлерам, вместо переменных замыкания.
11 файлов подключаются классическими <script> без type="module" (Web/ui/index.html:474). Итог: 707 объявлений верхнего уровня живут в общем window.
bootstrap.jsобъявляетconst app = ...,layout.jsего использует — связь только через порядок тегов.- Отсюда защитные
updateTreeScrollbar?.()(Web/ui/layout.js:19) — код страхуется от того, что зависимость ещё не загрузилась. - Манифест ассетов продублирован:
scriptNamesвWeb/src/cache-ui-assets.ts:20и<script src>вindex.html. Они уже разошлись (pdf-viewer.jsесть только в первом). - Ни ESLint, ни
checkJs—tsconfig.jsonпокрывает толькоsrc/**/*.ts. 500KB кода вообще без статической проверки. Cache-Control: no-storeна все ассеты + чтение всех файлов синхронно на импорте модуля.
Что сделать: перевести на нативные ES-модули (<script type="module" src="/ui/main.js">, import/export между файлами) — бандлер не нужен, Electron 42 это тянет. Дальше: allowJs + checkJs в tsconfig, JSDoc-типы на публичных функциях, ESLint с правилом no-undef. Манифест ассетов вывести из одного источника — либо генерировать список из index.html, либо отдавать директорию через whitelist по расширению.
Самая дорогая проблема, потому что она маскирует все остальные.
Web/tests/cache-ui-assets.test.ts— 1266 строк, 111 вызововreadFileSync, 37 извлечений видаindexOf('function ...')с последующимvm.runInNewContextнад вырезанным куском исходника.- Ассерты вроде
assert.equal(viewerScript.includes("if (activeFindKind === 'main' && mainSearch.key) return;"), true)(:41) — это тест на форматирование строки, а не на поведение. Он и упал. assert.equal(html.includes('const app = document.getElementById'), false)(:16) — тест «строки нет в файле».- Та же техника в
inductive-markings.test.tsиparallel-translations.test.ts.
Это не выбор, а следствие проблемы №3: раз ui/*.js не модули — импортировать функцию нельзя, остаётся вырезать её текстом. После перевода на ES-модули ~90% этих тестов заменяются обычными import { referencePattern } from '../ui/pdf-viewer.js' и проверками на входах/выходах.
Отдельно: в e2e добавился await page.waitForTimeout(1200) (Web/e2e/specs/notes/pdf-viewer.spec.ts:207), завязанный на новый таймер в 900 мс. Хардкод-сон под конкретную константу продакшн-кода — гарантированная будущая флака. Лучше expect(...).toHaveCount(1) с toPass() / ретраями.
Web/ui/api-page-view.js:267–320 — программа примерно на 50 строк JS, собранная конкатенацией строк, с '<scr' + 'ipt>', экранированием \\\" в четыре слэша и вставкой через insertAdjacentHTML в документ с недоверенным OneNote-HTML.
Ни подсветки, ни синтаксической проверки, ни возможности протестировать, ни стек-трейсов при ошибке. Плюс внутри неё:
new RegExp(mainSearchSource, mainSearchFlags)(:290) — паттерн и флаги приходят изpostMessageбез валидации.- Ни одной проверки
event.origin:grep event.origin ui/*.jsдаёт 0 совпадений, при том что всеpostMessageидут сtargetOrigin: '*'(25+ мест).
Настройки Electron при этом сделаны правильно (contextIsolation:true, nodeIntegration:false, sandbox:true, setWindowOpenHandler → deny в Web/src/electron-main.ts:849 и :899), и iframe для обычных страниц идёт с sandbox="allow-scripts" (Web/ui/tree-pages.js:2132) — то есть периметр держится. Но модель «доверяем всему, что прилетело в message» держится только на этом периметре.
Что сделать: вынести мост в обычный файл ui/page-frame-bridge.js, подключать его в srcdoc через <script src="/ui/page-frame-bridge.js">. В обработчике message проверять event.source === parent. Вместо передачи сырых source/flags передавать структурированный запрос (строка + набор булевых опций) и собирать regex на стороне фрейма с экранированием.
Web/src/cache-schema.ts:236–364 — 25 блоков вида if (!columns.some(c => c.name === 'x')) db.exec('ALTER TABLE ... ADD COLUMN x').
Работает, но:
- нет номера версии схемы — в диагностике не видно, на какой ревизии база;
- нет защиты от отката — старая сборка молча откроет новую базу;
- бэкфилы данных вшиты внутрь проверки на отсутствие колонки (
:241), то есть семантика миграции зависит от состояния схемы, а не от версии; - на каждом старте выполняется серия
PRAGMA table_info.
Что сделать: массив миграций + PRAGMA user_version:
const migrations: Array<(db: Database) => void> = [ /* 1..N */ ];
const current = db.pragma('user_version', { simple: true }) as number;
if (current > migrations.length) throw new Error('Кэш создан более новой версией приложения');
for (let v = current; v < migrations.length; v++) {
db.transaction(() => { migrations[v](db); db.pragma(`user_version = ${v + 1}`); })();
}Заодно: в системе сейчас две независимые SQLite-базы с двумя разными механизмами миграций — EF-миграции в Api/Persistence и ручные ALTER в Web. После проблемы №1 останется одна.
Web/src/cache-search.ts:7: getCachedPage = SELECT * FROM pages WHERE id = ?. Таблица pages хранит content_html и content_text — то есть любой запрос метаданных тянет весь HTML страницы.
Новый код это усугубляет: Web/src/cache-ui.ts:1525 вызывает getCachedPage ради одного поля content_html в маршруте, который до этого работал только с параграфами.
Что сделать: разделить на getPageMeta (явный список колонок без тел) и getPageContent(db, id, 'html' | 'text'). Проверить остальные вызовы getCachedPage — большинству тело не нужно.
Один и тот же блок скопирован 25 раз (Web/src/cache-ui.ts:856, 863, 875, 886, 922, …):
const ownOrigin = `http://${request.headers.host ?? `127.0.0.1:${options.port}`}`;
if (request.headers.origin && request.headers.origin !== ownOrigin) {
return json(response, 403, { error: '...' });
}Сейчас покрыты все 25 мутирующих маршрутов — проверено. Но:
- 26-й маршрут добавится без неё, и это никак не проявится;
- проверка fail-open — при отсутствии заголовка
Originзапрос проходит.
Что сделать: после введения роутера (проблема №2) — один guard, применяемый ко всем не-GET маршрутам по умолчанию, с явным opt-out. Условие поменять на «Origin отсутствует ИЛИ совпадает с собственным» только если это действительно нужно для non-browser клиентов; иначе требовать заголовок.
Web/%SystemDrive%/ProgramData/Microsoft/Windows/Caches/*.db — 4 файла кэша Windows Shell, закоммичены в 9a48ba0. Какой-то скрипт использовал %SystemDrive% в контексте, где cmd-переменная не раскрывается, и создал каталог с таким именем прямо в репозитории.
git rm -r --cached "Web/%SystemDrive%" && rm -rf "Web/%SystemDrive%"Найти и починить источник — вероятно, путь собирается в PowerShell или Node, где нужно $env:SystemDrive / process.env.SystemDrive.
bibleConfig.apiUrl.replace(/\/+$/, '')повторяется 11 раз вWeb/src/biblenote-gateway.ts— нормализовать один раз при чтении конфига.vendor/в.gitignore, ноdist:winот него зависит. Сборка из чистого клона невозможна без ручного прогона stage-скриптов — стоит явно описать в README или зафиксировать версии артефактов.Site/не в git вообще. Если это деплоймент-таргет, его надо закоммитить; вSite/_headersнетContent-Security-Policy..gitignoreигнорирует*.sqliteи*.db-wal/*.db-shm, но не*.db.
Сервер (Web/src/html.ts:44) нумерует вхождения по порядку <a> внутри <section data-pdf-page> в кэшированном HTML. Клиент выбирает matches[occurrenceIndex] из совпадений regex по текстовому слою PDF. Это два разных текста: в текстовом слое «Ин 3:16» может встречаться и там, где в HTML ссылки нет. Тогда N-я ссылка ляжет не на своё место.
Плюс регрессия: matches.slice(i, i + 1) даёт ровно одного кандидата, и если он перекрыт диапазоном более длинной ссылки (occupied.some(...) → continue), оверлей теряется полностью. До правки перебирались все совпадения и находилось свободное.
Предложение — оставить occurrenceIndex как приоритетную подсказку, но с откатом на прежний перебор:
const preferred = Number.isInteger(occurrenceIndex) && occurrenceIndex >= 0 && matches[occurrenceIndex]
? [matches[occurrenceIndex], ...matches.filter((_, i) => i !== occurrenceIndex)]
: matches;
for (const match of preferred) {
const start = Number(match.index) || 0;
const end = start + match[0].length;
if (occupied.some(range => start < range.end && end > range.start)) continue;
occupied.push({ start, end });
addBibleLinkRect(layer, overlay, match, link, mapped.positions);
break; // одна ссылка — один прямоугольник
}!Number.isInteger(Number(link.pageNumber)) пропускает undefined (Number(undefined) → NaN), но null даёт Number(null) === 0 → Number.isInteger(0) === true → ссылка отфильтруется на всех страницах. Сейчас сервер всегда шлёт целое, но JSON-null тихо убьёт все оверлеи.
Явнее: link.pageNumber == null || Number(link.pageNumber) === renderedPageNumber.
Надо либо обновить под новую строку, либо (правильнее) заменить на проверку поведения runTargetSearch с forceTarget.
- Удалить мёртвую .NET/Angular ветку (проблема №1).
- Перевести
ui/*.jsна ES-модули (проблема №3). - Переписать source-scraping тесты на нормальные (проблема №4).
- Разбить
cache-ui.tsна роутер + маршруты (проблема №2), заодно закрыв №8.
Пункты 3 и 4 без пункта 2 сделать нормально не получится.