feat(assistant): наблюдаем RAG fallback — статистика на retrieval-а и логнати деградации - #321
feat(assistant): наблюдаем RAG fallback — статистика на retrieval-а и логнати деградации#321nedda76 wants to merge 37 commits into
Conversation
ydimitrof
left a comment
There was a problem hiding this comment.
Обобщение на ревюто (feat(assistant): наблюдаем RAG fallback)
Много добре изпипано и изчерпателно тествано PR. Промяната добавя наблюдаемост на retrieval-а (RetrievalStats/onStats, логнати деградации в route-а — #318), маха последния as unknown as каст чрез боундъри-адаптер (bindings.ts — #316), сменя метаданни-filter с нативни версионирани namespace-и (schema-v2/entity-v1 — #317) и въвежда безусловно инжектиране на hard-trap-овете плюс relevance floor за RAG. Придружено е с целенасочени тестове за всеки нов клон.
Фаза 0 — security scan: ЧИСТО
- Няма закодирани тайни —
BGGPT_API_KEYминава презwrangler secret put, изрично никога не се комитва. - Няма нови/променени външни URL-и извън одобрените.
- Няма backdoor/обфускация/инжекции. Точно обратното — денилистът в
sql-guard.tsсе разширява (group_concat/string_agg/json_group_array/json_group_object), аreport-schema.tsзатваря дупка за неограничени числа в публичен отчет. - Промените в зависимостите са само в
osv-scanner.toml(игнориране наsharplibvips CVE-та) — обосновано като транзитивна, само-dev зависимост на miniflare, извън деплойнатия Worker, с дата за преразглеждане.
Силни страни
- Сигурност/устойчивост: логовете носят само броячи и
error.message, никога текста на въпроса или payload-а (последователно вbindings.ts,rag.ts,assistant.chat.tsx) — правилна защита срещу изтичане на потребителски вход в логовете. - Коректност:
alignе whitelist-нат, spread-ът на колони е заменен с експлицитно копиране (не пропуска непознати model-подадени полета към рендера), array-length таваните късат сканирането при препълване,score ?? 0защитава срещу TypeError надолу по веригата. - Без дублиране:
renderTraps()е единствен източник за trap-овете; тестът „exactly-once" покрива регресията с двойно рендиране през реалния write→read seam. - Тестове: смислени, а не тривиални — derived-from-floor фикстури, за да не се чупят при рекалибрация на
MIN_SCHEMA_SCORE; проверки на call-count, за да не се промъкне filter-базиран fallback.
Съображения (не блокиращи)
MIN_SCHEMA_SCORE = 0.35е некалибриран за v2 корпуса (изрично отбелязано, #318). Рискът е контролиран: под флора →[]→ безопасен fallback към пълния речник, а fallback-процентът вече е наблюдаем. Препоръка: рекалибрирайте с реални въпроси, преди да разчитате на RAG grounding в прод.- Почистване на стар кохорт изисква ръчно възстановяване на списъка с id-та от git историята на
buildSchemaChunks— операционно чупливо, но документирано и незадължително (namespace-ът изолира старите кохорти). - Денилистът за SQL функции по същество е догонваща игра — авторите сами отбелязват, че позитивен allowlist е трайното решение (проследено отделно).
Препоръка: COMMENT
Кодът е готов за мърдж и не чупи main. Оставям COMMENT (вместо APPROVE) единствено заради висящата рекалибрация на relevance floor-а (#318) — тя е предпоставка да се вярва на RAG grounding в новия режим, макар че текущото поведение при провал е безопасно.
8a8156f to
8410e8d
Compare
ydimitrof
left a comment
There was a problem hiding this comment.
Обобщение на ревюто — feat(assistant): наблюдаем RAG fallback
Обща оценка: 9.3/10 — APPROVE. PR-ът е с висока чистота, добре тестван и защитен на всяка граница. Промените са атомарни и в един ясен концерн (наблюдаемост на RAG + версионирани namespace-и + няколко review follow-up хардънинга).
Сигурност (Фаза 0 — автоматично сканиране)
- Няма hardcoded тайни.
wrangler secret put BGGPT_API_KEYе интерактивен; README изрично казва „никога не се комитва". - Няма нови/променени външни URL-и. Единственият модел-литерал е
@cf/baai/bge-m3(Workers AI capability), не мрежов endpoint. - Няма зловредни шаблони / обфускация / backdoors.
- Зависимости:
osv-scanner.tomlдобавя ignore само за транзитивна dev-only уязвимост (sharp<0.35.0 през miniflare), с обосновка иignoreUntilдата. Приемливо — не е в деплойнатия Worker. - Резултат: CLEAN.
Силни страни
emit-report-schema.ts— тавани на масивите (MAX_BLOCKS/ITEMS/COLUMNS) със short-circuit преди per-element сканирането; тестовете доказват, че се връща точно кап-грешката, а не 101 паразитни грешки. Затваря реален DoS/amplification вектор.isAlignwhitelist + експлицитно изграждане на колоните вbindReport(без{...c}spread) — спира пренасяне на непроверени model-подадени полета към рендера. Отлична defense-in-depth срещу атрибутна инжекция.sql-guard.ts— блокиране на string-building агрегати (group_concat/string_agg/json_group_*) — правилно затваря същия memory-amplification клас катоprintfедно ниво нагоре, вкл.string_aggalias за SQLite ≥3.44 (D1).system-prompt.ts—hardTraps()безусловно — коригира реалната регресия „RAG turn с по-малко ограничения от no-RAG fallback". Композиционният тест през реалния write→read seam (indexSchemaCorpus → retrieve → buildSystemPrompt) проверява „точно веднъж" за всеки trap — силен тест.bindings.ts— премахване наas unknown as(issue #316) с реален compiler-checked bridge и keys-only диагностика, която не логва потребителски текст.- Наблюдаемост (#318):
RetrievalStatsс три отделни брояча (matched/aboveFloor/kept) различава двата класа деградация; best-effort try/catch около sink-а; логовете носят само броячи, никога текста на въпроса.
Незадължителни watch-items (не блокират мърдж)
MIN_SCHEMA_SCOREиMIN_ENTITY_SCORE= 0.35 (rag.ts). Кодът сам отбелязва „RECALIBRATION PENDING (#318)": прагът е калибриран срещу pre-v2 корпуса (12 къси trap chunk-а), а v2 е 25 по-дълги query/table chunk-а, които скорират различно под bge-m3. Рискът е тих fallback към пълния речник за много въпроси — но е безопасният изход и вече е наблюдаем чрезassistant.ragлога. Препоръка: измерете реалните разпределения на скора преди да фиксирате прага; обмислете отделни стойности за схема vs entity.entity-v1namespace е празен by design (няма entity indexer). Инструментът връща 0 попадения — коректно документирано в README/rag.ts, включително предупреждението, че „WHEN TO BUMP" правилото НЕ се пренася към data-derived entity корпуса. Добра проактивна бележка за бъдещия indexer.- Denylist подходът в
sql-guard.tsе inherently catch-up игра срещу нови aliases — PR-ът сам признава, че positive allowlist е трайният фикс (tracked separately). ОК за сега.
CLAUDE.md съответствие
Няма partial implementation, TODO-simplification, дублиране (renderTraps() дедуплицира trap рендера между describeSchema и hardTraps), мъртъв код или resource leaks. Тестовете са смислени (не cheater — проверяват граници, floor drop-ове, метадата bug сигнали), не тривиални. Именуването е консистентно.
Препоръка: APPROVE. Watch-items-ите са наблюдателни follow-up-и, не дефекти.
8410e8d to
4ab2b80
Compare
ydimitrof
left a comment
There was a problem hiding this comment.
Ревю на PR: наблюдаем RAG fallback — статистика на retrieval-а и логнати деградации
Фаза 0 — Сигурност (блокираща): ЧИСТО ✅
- Няма твърдо кодирани тайни.
BGGPT_API_KEYминава презwrangler secret putи изрично се коментира „никога не се комитва". Няма API ключове/пароли/токени в дифа. - Няма нови външни URL-и.
EMBED_MODEL = '@cf/baai/bge-m3'е Workers AI идентификатор на модел, не мрежов адрес. - Няма зловредни шаблони (backdoor/инжекция/обфускация).
- Зависимости: единствената промяна е добавяне на два
IgnoredVulnsзаписа вosv-scanner.tomlзаsharp<0.35.0(libvips CVE-та). Обосновката е коректна — транзитивна, само-dev зависимост през miniflare, не влиза в деплойнатия Worker, няма in-range upstream fix; имаignoreUntilдата за ре-триаж. Приемливо.
Общо качество: много силен PR
Промяната е атомарна и добре обоснована — цялата се върти около една тема (наблюдаемост на RAG fallback + втвърдяване на границите). Забележителни силни страни:
- Сигурност на данните към логовете: навсякъде се логват само броячи/съобщения, никога текстът на въпроса или суровият error обект (
bindings.ts,assistant.chat.tsx,rag.ts). Това е последователно спазено и изрично документирано. bindings.ts(issue #316): премахването наas unknown asв полза на типизиран адаптер е реално подобрение — грешка на границата вече се хваща отtsc, а не в продукция. Адаптерът разграничава „липсващ ключ" от „празенdataмасив за непразен вход", което пази долнатаembed()count-проверка от подвеждащо съобщение.rag.ts: преходът от metadatafilterкъм нативенnamespaceе правилен (metadata филтър изисква provisioned metadata index, който репото няма — #317). Версионирането на namespace + id (schema-v2) и правилото „WHEN TO BUMP" коректно пазят rollback прозореца. Релевантностният праг (MIN_SCHEMA_SCORE) решава реален проблем — иначе top-K връща K-те най-близки чънка дори при изцяло off-topic заявка и прави grounding-а по-слаб от no-RAG fallback-а.system-prompt.ts:hardTraps()инжектира императивните капани безусловно — това затваря реалната дупка, при която RAG turn можеше да остане с по-малко ограничения от fallback-а. Тестътrenders every hard trap exactly onceминава през реалния write→read seam и пази срещу двойно рендиране.sql-guard.ts: разширяването с string-building агрегати (group_concat/string_agg/json_group_*) и покриването на quoted-identifier bypass ("group_concat"(x),[group_concat](x)) е добре обмислено; авторът честно отбелязва, че denylist е catch-up игра и позитивен allowlist е трайният fix.- Покритие с тестове: отлично. Всеки нов клон има целеви тест (праг, scoreless match, onStats broene, throwing sink, cap short-circuit, quoted-name bypass, spelled magnitudes до секстилион). Тестовете са проектирани да ловят регресии, не да минават тривиално.
CLAUDE.md съответствие
Няма частична имплементация, дублиране (renderTraps() дедупlicира rendering-а на капаните), мъртъв код или смесени концерни. Ресурсите се почистват (best-effort try/catch около onStats).
Дребни, неблокиращи бележки
semanticSearchне получиonStatsнаблюдаемостта, коятоretrieveSchemaContextпридоби. Днес е безвредно (entity корпусът е празен по дизайн), но когато entity indexer-ът от Фаза 2 се появи, ще е полезна същата симетрия (виж инлайн).MIN_SCHEMA_SCOREе изрично отбелязан за рекалибрация (#318) спрямо v2 корпуса — това е следеният, очакван компромис, а не скрит TODO; просто да не се забрави преди да се разчита на прага в новия режим.
Вердикт
Няма блокиращи или critical находки. Сигурност: чисто. Тестове, наблюдаемост и документация са налице и последователни. Препоръка: APPROVE.
…isFinite и по схема пътя
- semanticSearch подава SemanticSearchStats (matched/kept) през същия
best-effort reportStats hook; tools.ts ги логва структурирано
({evt:'assistant.semantic'}) — след напълването на entity-v1 (Фаза 2)
'флорът отряза всичко' и 'празен namespace' са различими и там.
- Схема флорът също мина на Number.isFinite (симетрия с entity ръба при
изричен minScore = 0 в RetrieveOptions).
- Error логът на semantic_search подава само message — провайдърски
error envelope може да ехне текста на заявката (същата дисциплина
като route-а). (бележки от ревюто на midt-bg#321)
4ab2b80 to
f5ef473
Compare
|
Малка непоследователност в leak-safety патерна, който този PR въвежда: Заварено (непроменено спрямо main), но след като серията установява и тества „само Иначе #319→#320→#321 е издържана серия: namespace-ът е приложен и на insert, и на query ( |
…isFinite и по схема пътя
- semanticSearch подава SemanticSearchStats (matched/kept) през същия
best-effort reportStats hook; tools.ts ги логва структурирано
({evt:'assistant.semantic'}) — след напълването на entity-v1 (Фаза 2)
'флорът отряза всичко' и 'празен namespace' са различими и там.
- Схема флорът също мина на Number.isFinite (симетрия с entity ръба при
изричен minScore = 0 в RetrieveOptions).
- Error логът на semantic_search подава само message — провайдърски
error envelope може да ехне текста на заявката (същата дисциплина
като route-а). (бележки от ревюто на midt-bg#321)
…а модула Третият catch в route-а още логваше суровия error обект, докато същият файл вече два пъти пази 'само message' заради ехо на въпроса в логовете. Същият клас имаше и в run_sql (D1 грешка носи SQL-а, построен от въпроса) и в stream onError (BgGPT грешка носи prompt-а). Вместо трети copy-paste на тернара — errorText() в log-safety.ts, използван на ВСИЧКИТЕ пет catch/лог места: само message (никога stack, никога cause веригата, никога обекта), с таван от 300 знака срещу гигантско тяло от провайдър. Тестът закова точно тези инварианти (негативен контрол: връщане на stack/cause го чупи). Бележка от ревюто на @lyubomir-bozhinov по midt-bg#321.
f5ef473 to
c7ec503
Compare
|
Прав си — и не само третият catch. Проверих всички лог места в модула и същият клас беше още на две: Затова вместо трети copy-paste на тернара — Благодаря и за прегледа на серията — merge редът е потвърден: 319 → 320 → 321. |
|
Допълнение към горното — само-ревюто на първия ми комит намери, че той пазеше грешната ос, затова го поправих в
Плюс: collapse на нови редове (многоредово съобщение чупеше prefix-базиран grep — продълженията нямат Негативни контроли: махането на try/catch чупи теста за тоталност, махането на редакцията — теста за ехото. 524 теста зелени, покритието над baseline. Остава като съзнателен пропуск: корелация на |
|
Патчовете Един остатъчен ръб в let out = raw.replace(/\s+/g, ' ').trim();
for (const needle of redact) {
if (needle && needle.length >= MIN_REDACT_CHARS) out = out.split(needle).join(REDACTED);
}
Repro: const question = 'колко плати\nобщина Пловдив на фирма Х';
errorText(new Error(`400 invalid input: ${question}`), [question]);
// → "400 invalid input: колко плати община Пловдив на фирма Х" (нередактирано)Фикс — свий needle-а по същия начин преди const n = needle.replace(/\s+/g, ' ').trim();
if (n.length >= MIN_REDACT_CHARS) out = out.split(n).join(REDACTED);Minor (само server логове, иска ехо от provider-а), но пада точно в основния control за ехнат вход — и е един ред тест: needle с |
|
Приложено в Покрай това минах клона още веднъж отгоре до долу — гаранцията „въпросът не влиза в лога" беше пробита на още три места, всички затворени:
Всичко това беше минало под радара, защото нито един тест не влизаше през вратата на потребителя — добавени са route-door тестове (POST през Останалото от само-ревюто ( 549 теста зелени, |
…isFinite и по схема пътя
- semanticSearch подава SemanticSearchStats (matched/kept) през същия
best-effort reportStats hook; tools.ts ги логва структурирано
({evt:'assistant.semantic'}) — след напълването на entity-v1 (Фаза 2)
'флорът отряза всичко' и 'празен namespace' са различими и там.
- Схема флорът също мина на Number.isFinite (симетрия с entity ръба при
изричен minScore = 0 в RetrieveOptions).
- Error логът на semantic_search подава само message — провайдърски
error envelope може да ехне текста на заявката (същата дисциплина
като route-а). (бележки от ревюто на midt-bg#321)
…а модула Третият catch в route-а още логваше суровия error обект, докато същият файл вече два пъти пази 'само message' заради ехо на въпроса в логовете. Същият клас имаше и в run_sql (D1 грешка носи SQL-а, построен от въпроса) и в stream onError (BgGPT грешка носи prompt-а). Вместо трети copy-paste на тернара — errorText() в log-safety.ts, използван на ВСИЧКИТЕ пет catch/лог места: само message (никога stack, никога cause веригата, никога обекта), с таван от 300 знака срещу гигантско тяло от провайдър. Тестът закова точно тези инварианти (негативен контрол: връщане на stack/cause го чупи). Бележка от ревюто на @lyubomir-bozhinov по midt-bg#321.
56a24a2 to
ea27b25
Compare
…рифт-пазач) Речникът, който моделът чете (describe-schema.ts), дрифтна тихо веднъж: amendments.contract_id никога не е съществувала, а parties.role — също, и двете се откриха само на око при ревюто на midt-bg#321. Всяка фантомна колона там влиза във всеки prompt, а полученият SQL пада в runtime с грешка, която моделът „поправя" с ново налучкване. Тестът прилага ВСИЧКИ packages/db/migrations/*.sql поред върху временна sqlite3 база (same harness като packages/db/src/migrations.test.ts) и проверява: - всяка таблица от речника съществува; - всеки водещ идентификатор от всеки columns низ е реална колона (pragma_table_info; скобените бележки със запетаи/кавички се игнорират); - всяка →таблица референция е реална таблица; - всяка канонична заявка КОМПИЛИРА срещу схемата (EXPLAIN) — тях моделът копира дословно като отправна точка; - негативни контроли: историческият дрифт ('id, contract_id→contracts, …') се хваща, а parser-ът не се лъже от бележки в скоби. Проверено срещу main-версията на речника: пазачът докладва точно amendments.contract_id и parties.role. Понеже поправката им живее в midt-bg#321, клонът е стакнат върху него (мърдж ред: midt-bg#321 → този). Тестът живее в apps/web/test/ и е включен в tsconfig.node.json (Node типове за child_process), за да не се наливат Node globals в Workers кода на приложението; vitest include покрива test/**.
…isFinite и по схема пътя
- semanticSearch подава SemanticSearchStats (matched/kept) през същия
best-effort reportStats hook; tools.ts ги логва структурирано
({evt:'assistant.semantic'}) — след напълването на entity-v1 (Фаза 2)
'флорът отряза всичко' и 'празен namespace' са различими и там.
- Схема флорът също мина на Number.isFinite (симетрия с entity ръба при
изричен minScore = 0 в RetrieveOptions).
- Error логът на semantic_search подава само message — провайдърски
error envelope може да ехне текста на заявката (същата дисциплина
като route-а). (бележки от ревюто на midt-bg#321)
…а модула Третият catch в route-а още логваше суровия error обект, докато същият файл вече два пъти пази 'само message' заради ехо на въпроса в логовете. Същият клас имаше и в run_sql (D1 грешка носи SQL-а, построен от въпроса) и в stream onError (BgGPT грешка носи prompt-а). Вместо трети copy-paste на тернара — errorText() в log-safety.ts, използван на ВСИЧКИТЕ пет catch/лог места: само message (никога stack, никога cause веригата, никога обекта), с таван от 300 знака срещу гигантско тяло от провайдър. Тестът закова точно тези инварианти (негативен контрол: връщане на stack/cause го чупи). Бележка от ревюто на @lyubomir-bozhinov по midt-bg#321.
ea27b25 to
cf78482
Compare
…рифт-пазач) Речникът, който моделът чете (describe-schema.ts), дрифтна тихо веднъж: amendments.contract_id никога не е съществувала, а parties.role — също, и двете се откриха само на око при ревюто на midt-bg#321. Всяка фантомна колона там влиза във всеки prompt, а полученият SQL пада в runtime с грешка, която моделът „поправя" с ново налучкване. Тестът прилага ВСИЧКИ packages/db/migrations/*.sql поред върху временна sqlite3 база (same harness като packages/db/src/migrations.test.ts) и проверява: - всяка таблица от речника съществува; - всеки водещ идентификатор от всеки columns низ е реална колона (pragma_table_info; скобените бележки със запетаи/кавички се игнорират); - всяка →таблица референция е реална таблица; - всяка канонична заявка КОМПИЛИРА срещу схемата (EXPLAIN) — тях моделът копира дословно като отправна точка; - негативни контроли: историческият дрифт ('id, contract_id→contracts, …') се хваща, а parser-ът не се лъже от бележки в скоби. Проверено срещу main-версията на речника: пазачът докладва точно amendments.contract_id и parties.role. Понеже поправката им живее в midt-bg#321, клонът е стакнат върху него (мърдж ред: midt-bg#321 → този). Тестът живее в apps/web/test/ и е включен в tsconfig.node.json (Node типове за child_process), за да не се наливат Node globals в Workers кода на приложението; vitest include покрива test/**.
The curated dictionary the model treats as hard fact had drifted from packages/db/migrations/0000_init.sql: - amendments: no contract_id column — it links via unp/contract_number - parties: no role column — real cols are party_key, eik, ocid, party_id, name… - value_flag enum was missing value_low - amount_eur IS NULL was described as meaning value_suspect; it actually has several causes (FX-rateless foreign / value_suspect w/o estimate / no signing+current), and the unconfirmed count is value_flag='value_suspect' (home_totals.suspect), not NULL-amount rows - data_freshness is a table, not a view Drift here misleads a weak model into wrong joins or a wrong integrity KPI.
…e retrieval Two grounding gaps that could leave a RAG turn LESS constrained than the no-RAG fallback: - buildSystemPrompt used the retrieved chunks INSTEAD of the dictionary, so a retrieval that missed the money-sum trap dropped the SUM(amount_eur) rule entirely. Inject the short imperative DATA_TRAPS unconditionally; RAG now only selects the extra tables/example-queries for the question. - retrieveSchemaContext had no relevance floor — top-K returned its K least-distant chunks even when all were off-topic. Add MIN_SCHEMA_SCORE; below it we return fewer/zero chunks, and zero falls back to the full dictionary (the safe outcome).
The Dependency-audit step (osv-scanner) fails on ANY known vuln and, per its own comment, expects intentional exceptions in osv-scanner.toml — which did not exist yet. sharp@0.34.5 (High, GHSA-f88m-g3jw-g9cj: inherited libvips decoder CVEs) has no in-range upstream fix: miniflare pins sharp ^0.34.5 and its latest release still ships 0.34.5, so 0.35.0 is unreachable without a forced override. sharp is a transitive dev-only dep (miniflare dev server / test runtime), absent from the deployed Worker, and the vuln needs decoding an untrusted image the toolchain never handles. Record a dated (ignoreUntil 2026-10-22) exception so the audit gate goes green and the entry auto-resurfaces for revisit. Verified locally with osv-scanner 2.4.0: fails without the config, passes with.
…d RAG retrieval DATA_TRAPS are injected into the system prompt unconditionally (hardTraps), so indexing them in the schema corpus let retrieval hand the same rule back as "context" and render it twice. Traps are no longer indexed, and retrieveSchemaContext drops kind:'trap' matches a previously deployed index may still hold. Retrieval's job stays selecting relevant tables/queries. (review note, ydimitrof)
The sharp entry used a TOML local-date (2026-10-22) while the react-router entry above uses full RFC3339; align on the latter so parser versions cannot read the file inconsistently. (review note, ydimitrof)
…ace instead of a runtime trap filter Self-review of the previous commit found the client-side kind:'trap' filter ran AFTER Vectorize's server-side topK cut, so legacy trap vectors (12 of ~37 in a pre-change index, and the most money-question-similar text in the corpus) could eat up to all six retrieval slots — leaving the turn with fewer tables/queries than the no-RAG fallback, silently and permanently, since upsert never deletes the stale ids. Replaced with a versioned NATIVE namespace (SCHEMA_NS = 'schema-v2') on both the upserted vectors and the query: native namespaces need no metadata index and exclude every stale cohort at the source, so no topK slot is ever spent on a discarded match and the filter is gone. The version is in the vector ids too, so re-indexing writes a new cohort and a Worker rollback keeps working against the old one. An un-reindexed environment gets zero matches → the documented full-dictionary fallback. Also from the self-review: the stale module header still said trap-rules are embedded; system-prompt tests fed trap strings retrieval can no longer produce; and no test entered through the composed seam — added a retrieveSchemaContext → buildSystemPrompt test seeded with the real corpus asserting every DATA_TRAP renders exactly once (negative-controlled: re-adding traps under a disguised id/kind fails it and the new corpus-length assertion). README provisioning now documents the re-index-on-bump requirement.
Gap-sweep on the namespace fix found the composed exactly-once test was weaker than advertised: it hand-mirrored the write mapping instead of running indexSchemaCorpus, sliced only the first topK chunks (so a trap appended at the corpus tail escaped it), and hard-coded a 0.9 score silently coupled to MIN_SCHEMA_SCORE. It now routes through the real write path into a recording fake, retrieves the WHOLE corpus, and derives its score from the floor — so the write→read metadata contract (text key, ids, namespace) is under test and a trap re-added at any position under any id/kind fails it (negative-controlled again with a tail-appended, table-kind trap). Also: remaining fixtures moved off pre-v2 unversioned ids; the semanticSearch test title no longer claims a namespace it does not use (it pins the entity METADATA filter); the module header no longer claims the bindings satisfy the structural types (the route casts — drift is not tsc-checked); the new-cohort rollback guarantee is now correctly stated as bump-only, with an explicit WHEN TO BUMP rule (in-place upserts, positional query ids, orphan risk); the README no longer suggests purging a cohort inside its rollback window and notes delete-vectors needs an explicit id list; dropped the stale '150 теста' verification claim.
… величините
Единствените near-collisions на суфиксния шаблон са думи на -лион
(напр. „Илион") — приети съзнателно: gate-ът нарочно флагва в повече,
а в регистъра на поръчките такива думи почти не се срещат. Записан е
изходът при евентуални фалшиви отхвърляния: \p{L} lookaround граница
(JS \b е ASCII-only), а не списък с изключения. Изброяването на
-илиард величините е сведено до реалните форми на „милиард"
(бележка от ревюто).
… gate-а „Дванадесет млрд. лева" нямаше нито цифра (за \d…млрд шаблона), нито пълнословен суфикс — изписано числително + абревиатура се промъкваше покрай целия gate. млрд/млн влизат в стем шаблона (флагват и без цифра; негативен контрол: тестът пада без промяната). Остатъкът „хил." без цифра остава приет — хилядите не са defamation-мащабният вектор (бележка от ревюто на midt-bg#320).
…enylist SQLite (D1) резолва "group_concat"(x), [group_concat](x) и `group_concat`(x) до същия built-in, а регексът изискваше голо име непосредствено пред скобата — цитиран идентификатор минаваше L1. Опционален quote клас след името затваря и трите форми (adversarial тестове; негативен контрол: падат без промяната). Идентификатор с padding в кавичките е РАЗЛИЧЕН за SQLite и не резолва built-in — не изисква обработка (бележка от ревюто).
midt-bg#317) Vectorize зачита metadata филтри само върху свойства с провизиран metadata index, а репото не провизира нито един — filter: { ns: 'entity' } на реален индекс греши или под-филтрира, и то тихо, защото извикващите поглъщат грешките. Native namespace-ът (entity-v1, версиониран като SCHEMA_NS) не изисква metadata index и се прилага преди всякакви филтри. Това беше последната употреба на metadata filter в модула. Entity корпус никога не е индексиран, така че няма legacy кохорт — бъдещият indexer трябва да upsert-ва с namespace: ENTITY_NS (README, „Какво остава"). Closes midt-bg#317
- Header-ът вече не твърди, че FTS инструментът search_entities съществува (само спецификация е) — semantic_search днес връща 0 попадения по дизайн, докато entity корпусът не се индексира. - ENTITY_NS коментарът и README вече НЕ пренасят правилото WHEN TO BUMP върху entity корпуса: то предполага ръчен append-only корпус, а entity корпусът е производен от данните — indexer-ът се нуждае от собствен reconciliation/delete път и трябва да пази id-тата си. - metadata.ns е маркиран изрично като форензично поле — НЕ филтруемо (няма metadata index); скоупингът е само през native namespace. - semanticSearch деградира match без score до 0 (същата защита като флора на retrieveSchemaContext) вместо TypeError в tools.ts; тест. - Тестовете за namespace коват и БРОЯ на заявките (toHaveBeenCalledTimes (1)) — иначе filter-базиран retry път би минал зелен.
…ема пътя Без флор, щом entity корпусът се напълни, top-K връща K-те най-близки съседа ДОРИ когато всички са off-topic, и те стигат до модела като реални hits. MIN_ENTITY_SCORE (симетричен на MIN_SCHEMA_SCORE) реже под прага; match без score се чете като под флора и отпада — същото защитно правило като схема пътя. Тестовете деривират скоровете от флора ± ε (бележка от ревюто на midt-bg#319).
…-namespace кохорта - Number.isFinite вместо ?? 0 във флор филтъра на semanticSearch: (undefined ?? 0) >= 0 промъкваше match без score като 'hit' при изричен minScore = 0, а истински score 0 при флор 0 е легитимен — двата случая вече са разграничени (+ тест). След филтъра score е гарантирано число и DTO-то няма нужда от fallback. - README: 'стар кохорт' изрично включва и оригиналния pre-namespace кохорт (id-та в DEFAULT namespace отпреди версионирането) — за първите среди той също е orphan за чистене (бележки от ревюто).
…s' на route границата (midt-bg#316) env.VECTORIZE вече се присвоява на VectorIndex БЕЗ каст: metadata на VectorRecord е стеснен до стойностите, които Vectorize приема, а неизползваемият metadata filter отпадна от интерфейса — така tsc доказва присвоимостта и дрейф между rag.ts и worker-configuration.d.ts чупи typecheck-а, не продукцията (негативен контрол: върнат filter член → TS2322 на самото присвояване). env.AI не може да удовлетвори EmbeddingRunner структурно (run() връща per-model union), затова route-ът минава през типизиран адаптер, който вика реалния @cf/baai/bge-m3 overload — също проверен от компилатора — и подава само embeddings члена; embed() и без това fail-fast-ва при малформен data. Кастовете към AgentEnv остават: BGGPT_API_KEY е secret и не присъства в генерирания Env — отделен въпрос от AI/Vectorize биндингите. Closes midt-bg#316
- EmbeddingRunner.run вече взима model: typeof EMBED_MODEL (литерала), а адаптерът го препраща в реалния Ai.run overload — втори, различен модел би бил компилационна грешка, не тихо embed-ване с грешния модел. - Адаптерът е изнесен в bindings.ts (embeddingRunnerFor) — единственият модул, който познава и двете страни на границата; unit тестван, включително [] случаят, който инлайн версията оставяше непокрит. - При неочаквана форма на отговора адаптерът хвърля именувана грешка (само ключовете, без payload — error envelope може да ехне въпроса) вместо да връща [], което се четеше като 'провайдърът не embed-на нищо'. - Header коментарът на rag.ts е разделен по интерфейс: VectorIndex е присвоим от VectorizeIndex (без каст), EmbeddingRunner нарочно НЕ е; поправено и погрешното твърдение, че Vectorize няма filter поле. - README provisioning gate-ът вика indexSchemaCorpus през embeddingRunnerFor — голият env.AI вече не typecheck-ва там.
…е като успех
[] е truthy — проверка само за присъствие връщаше { data: [] } за
непразен вход и embed() после обвиняваше '0 embeddings' вместо реалната
причина: провайдър, отговорил с празен batch. Празният масив вече е
именуван отделен случай в грешката на адаптера (+ тест; бележка от
ревюто на midt-bg#320).
…rieval-а + логнати деградации retrieveSchemaContext подава RetrievalStats (matched срещу kept) през опционален onStats hook; route-ът ги логва и вече не поглъща тихо грешката при retrieval (console.error + fallback, в стила на другите деградационни пътища на файла). kept=0 при matched>0 е точно тихият fallback към пълния речник, който досега беше неразличим от работещ RAG. Позиционните topK/minScore станаха opts обект — така сигнатурата носи и hook-а без опашка от undefined аргументи. Флорът MIN_SCHEMA_SCORE остава непипнат: коментарът му вече казва изрично, че е калибриран срещу пре-v2 корпуса и чака емпирично премерване — това е втората половина на midt-bg#318, която ще стъпи върху тези логове. Част от midt-bg#318
…вюто - onStats се вика в try/catch (reportStats) — хвърлящ metrics sink не може да струва на хода неговите чънкове (инвариантът на request-log.ts); закован с тест. - Третият деградационен път (!vec — embed без използваем вектор) вече също докладва, с нули — иначе е неразличим от 'статистиката не е вързана'. - kept се раздели от aboveFloor: aboveFloor > kept сигнализира счупен metadata.text контракт (бъг в индексирането), не флор — двата класа повреди имат противоположни поправки. - Логът на route-а е структуриран JSON (стилът на request-log.ts), а error логът подава само message — суров error обект може да ехне въпроса на потребителя в логовете. - Тестът за статистика деривира скоровете от MIN_SCHEMA_SCORE ± ε и затвърждава и върнатите чънкове, не само броячите. Част от midt-bg#318
…isFinite и по схема пътя
- semanticSearch подава SemanticSearchStats (matched/kept) през същия
best-effort reportStats hook; tools.ts ги логва структурирано
({evt:'assistant.semantic'}) — след напълването на entity-v1 (Фаза 2)
'флорът отряза всичко' и 'празен namespace' са различими и там.
- Схема флорът също мина на Number.isFinite (симетрия с entity ръба при
изричен minScore = 0 в RetrieveOptions).
- Error логът на semantic_search подава само message — провайдърски
error envelope може да ехне текста на заявката (същата дисциплина
като route-а). (бележки от ревюто на midt-bg#321)
…а модула Третият catch в route-а още логваше суровия error обект, докато същият файл вече два пъти пази 'само message' заради ехо на въпроса в логовете. Същият клас имаше и в run_sql (D1 грешка носи SQL-а, построен от въпроса) и в stream onError (BgGPT грешка носи prompt-а). Вместо трети copy-paste на тернара — errorText() в log-safety.ts, използван на ВСИЧКИТЕ пет catch/лог места: само message (никога stack, никога cause веригата, никога обекта), с таван от 300 знака срещу гигантско тяло от провайдър. Тестът закова точно тези инварианти (негативен контрол: връщане на stack/cause го чупи). Бележка от ревюто на @lyubomir-bozhinov по midt-bg#321.
…вариант пазеше грешната ос Само-ревюто на предишния комит намери три дефекта в него: 1. НЕ беше тотален: String(Object.create(null)) хвърля (проверено), а Proxy/хостилен message getter — също. Помощник, който живее в пет catch блока, не може да хвърля: изключението щеше да избяга от catch-а, да убие graceful fallback-а (пълния речник / стрийм линията) и да върне 500, при това с целия обект в лога на framework-а. 2. Филтрираше по грешната ос: махаше stack-а (който по конструкция НЕ носи потребителски текст) и пазеше message-а (където точно се появява ехото от провайдъра). Затова сега сайтовете, които знаят входа си, го подават за РЕДАКЦИЯ: въпросът (route + stream през ctx.userQuestion) и заявката (semantic_search). Това е частта, която реално затваря ехото; тапата и без-stack-а само ограничават щетата. 3. Тестът твърдеше 'обект се свежда до непрозрачен таг' — невярно за обект със собствен toString (проверено). Тестът вече документира реалното поведение, а не желаното. Освен това: collapse на нови редове (многоредово съобщение чупеше prefix-базиран grep — продълженията нямат [assistant] префикс), рязане по кодови точки (не режем емоджи на самотен surrogate), и връщане на stack frames там, където те са диагностиката — setup грешката в route-а е конфигурационна, не провайдърска. Негативни контроли: махането на try/catch чупи теста за тоталност, махането на редакцията чупи теста за ехото.
…о на съобщението
errorText свиваше съобщението до един ред преди split, но подаваният needle
(въпросът) оставаше суров — latestUserText прави само join(' ').trim(). Въпрос с
shift-enter, таб или двоен интервал, ехнат от провайдъра, вече не съвпадаше и
влизаше дословно в лога — точно гаранцията „no verbatim copy", която модулът
декларира.
Сега needle-ът минава през същото collapseWhitespace, а прагът MIN_REDACT_CHARS
се преценява по свитата форма (whitespace не може да издуе къс needle над него).
typeof guard пази тоталността срещу не-низ през unknown границата.
Тестове: repro на ревюто (needle с \n/\t/двоен интервал → «редактирано»),
floor по свитата форма, тоталност при не-низ. Негативен контрол: repro тестът
пада срещу старата имплементация.
…d/отрязано ехо Две дупки в log-safety, намерени при ревю на клона: 1. stackHead приемаше, че съобщението заема само ред 0 на stack-а. V8 печата ЦЯЛОТО (многоредово) съобщение преди първата рамка, така че `.slice(1, 1 + frames)` връщаше продълженията на съобщението — нередактирани и без таван — точно до редактирания errorText на същия ред в route-а. Сега се котви на първия ` at ` ред и пази само рамки; без разпознаваема рамка (друг runtime, hostile getter) връща '' — fail closed. 2. errorText редактираше само дословно (whitespace-свито) копие на needle-а. Провайдър, който връща JSON тялото като message, ехва входа escaped (`"` → `\"`, нов ред → двата знака `\n`); embed пътят праща отрязан вход; и двете минаваха покрай split-а. Сега се търсят и JSON-escaped формата на суровия needle, и прозорци от 24 знака за дълъг needle (отрязано ехо се бланкира като един run). Plus: суровото съобщение се отрязва до 19 200 UTF-16 единици (surrogate-safe) ПРЕДИ O(n) паса — многомегабайтово тяло вече не струва стотици ms в catch блок; capCodePoints пропуска Array.from при къс вход. Тестове: многоредово съобщение → нито ред от него в stackHead; stack без рамки → ''; JSON-escaped ехо и отрязано ехо → «редактирано»; дълъг needle при пре-cap → един run; несвързано съобщение непокътнато; surrogate на границата на пре-cap-а. Негативен контрол: 4 от новите тестове падат срещу старата имплементация.
… тестове през вратата на потребителя Ревю на клона намери, че „въпросът не влиза в лога" не е вярно на три места, въпреки errorText: 1. streamText има СОБСТВЕН default onError = console.error(error) — суровият APICallError с requestBodyValues (system prompt + съобщенията на потребителя) и responseBody. agent.ts подаваше onError само на toUIMessageStreamResponse, така че SDK-то продължаваше да пише суровия обект при всяка провайдърска грешка. Сега hook-ът е и на streamText (една редактирана линия на грешка, дедуп по идентичност), а UI hook-ът само решава какво вижда клиентът. Бонус: линията носи класа и HTTP статуса (и през RetryError) — 401 vs 429 vs 5xx пак са различими, само идентификатори, никога тяло. 2. Route-ът: catch-ът на RAG retrieval викаше errorText(error) БЕЗ needle, а точно той embed-ва въпроса — най-вероятното място за ехо. Вече подава [question] като останалите три места. 3. bindings.ts: `'data' in out` хвърля при не-обект, а V8 слага операнда в съобщението на TypeError-а — gateway, върнал 200 с текст/примитив, вкарваше тялото в лога през „keys-only" пътя. Сега се именува само типът. Тестове през вратата на потребителя (CLAUDE.md: „at least one test must enter through the same door as the user"): apps/web/app/routes/assistant.chat.test.ts кара POST-а през action() с фалшиви AI/VECTORIZE — stats линията е само броячи, RAG-провалът с ехо е редактиран, setup-провалът дава 503 с редактирана и само-рамки линия. agent.stream-error.test.ts кара runAssistant през реалния streamText с MockLanguageModelV3 — точно една низова линия, никога суровият обект. Негативни контроли: без needle-а route тестът пада; срещу старото agent.ts console.error се вика два пъти (втория път с обекта).
…аторът затваря sub/null/link Четири дупки от ревюто на клона по gate-овете на отчета и SQL guard-а: - Прозата: голият суфикс -илион хващаше „павилион(и)" — рутинен предмет на поръчка — и отхвърляше легитимно заглавие като несвързано число, което моделът не може да пренапише. Суфиксите вече са котвени към числителните представки (м/б/тр/квадр/квинт/секст/септ/окт/нон/дец) — затворено нагоре до 10^33, „Илион" и „билярд" вече не флагват, „милионер" още (безопасната посока). - SQL guard: json_group_array/object бяха в денилиста, но JSONB близнаците им (SQLite ≥3.45, в build-а на workerd) минаваха през двата слоя — същата класа „цял scan в една клетка". Регексът е jsonb?_group_(?:array|object). - emit-report-schema: `sub` на facts не се проверяваше по тип — число минаваше shape-а и хвърляше TypeError в bindReport (непрозрачна tool грешка към модела вместо retryable съобщение, и несканирана цифра). `align: null`/`link: null` („не е зададено" на модела) вече се приемат като отсъстващи, вместо да обръщат иначе валиден отчет в retry. Трикратният copy-paste на cap guard-а е един cappedArray helper със същите съобщения (диференциално проверени). - bindReport: `link` се пресъздава от двете си известни полета, а не се копира по референция — допълнителен ключ вътре (href) иначе влизаше в замразения отчет въпреки „само известни полета стигат до рендера". Тестове за всяко; негативен контрол: 5 нови теста падат срещу старите файлове.
…агва само след числително Две находки от втория преглед на клона: - describe-schema: новият запис за `amendments` изброяваше value_before/after/ delta като обикновени колони, без да каже, че са в `currency` (не в EUR — не се сумират между валути), че `value_suspect = 1` редовете носят удвоена, ненадеждна стойност, която самият сайт бланкира (миграция 0007), и че връзката към contracts е по ДВЕ колони (t.source_id = am.unp AND c.contract_number = am.contract_number) — само unp дава декартово преброяване. Точно класата SUM(amount) капан, заради която речникът съществува, отворена върху таблицата, която PR-ът току-що направи привлекателна. Записът вече носи и value_suspect, и value_restated. - report-schema: голите стемове `млрд|млн` (добавени за „дванадесет млрд.") флагваха и чисто мерни заглавия като „Стойност (млн. €)" — стилът на собствените колони на сайта, без никакво число — и обръщаха валиден отчет в retry, докато „(хил. €)" минаваше. Абревиатурата флагва само след изписано числително (затворен клас: 1–19, десетици, стотици + няколко/ десетки/стотици); „Сто-йност" не е „сто" (word-edge lookaround). Тестове: двете посоки за млн/млрд (числително → флаг; само единица → не), bindReport с header „Похарчено (млн. €)" минава.
…ции вместо позиционни за semantic_search - reportStats ловеше само синхронен throw; `(stats) => void` приема и async sink, чийто reject избягваше като unhandled rejection (runtime-ът го пише като грешка — обратното на best-effort). Върнатият promise вече се обезврежда, извикването остава синхронно (старият тест за хвърлящ sink пак минава). - Нито един тест не заковаваше `returnMetadata: 'all'`: фалшивият индекс връщаше metadata.text без да е поискан, така че махането на опцията оставаше зелено, докато реалният Vectorize (default 'none') дава text-less съвпадения, kept=0 и тих fallback към пълния речник на всеки ход. Двата namespace теста го pin-ват, а фалшивият индекс в system-prompt.test дава metadata само при 'all' — seam-ът вече доказва и четящата страна. - semanticSearch взимаше (topK, minScore, onStats) позиционно, докато сестринската retrieveSchemaContext — опции обект; единственият извикващ пишеше `undefined, undefined, cb`. Вече SemanticSearchOptions, огледално. - Фикстурите на два floor теста бяха твърди 0.6/0.1 — рекалибрация (midt-bg#318) над 0.6 щеше да ги събори за несъществуваща регресия; изведени от константата. - tools.test: semantic_search през runTool — рендер на hit-овете, stats линията само броячи, needle-ът на tool-а редактира ехо на заявката. - README/rag.ts: „редакция на текст минава без bump" подвеждаше — retrieval-ът връща metadata.text от индексирането, така че ВСЯКА промяна в корпуса иска повторно indexSchemaCorpus; bump-ът решава само нов кохорт vs. презапис. Таблицата на модулите вече изброява bindings.ts и log-safety.ts. Негативни контроли: старият reportStats → 1 unhandled rejection уловен; без returnMetadata → 3 теста падат.
…-ва до 0.35.3 Записът за GHSA-f88m-g3jw-g9cj идваше от стар комит (преди midt-bg#226), пребазиран върху main, който междувременно ИЗТРИ същото изключение и добави `sharp: '^0.35.3'` override в pnpm-workspace.yaml; lock-ът резолва само sharp@0.35.3. Обосновката („forcing 0.35.0 would require a pnpm override") описва точно това, което repo-то вече прави, а според собственото правило на файла („if the offending package has since been raised … DELETE the entry") записът не бива да съществува — keyed по advisory, би маскирал истинска регресия до 2026-10-22, ако override-ът някога отпадне. Връща файла към main (и маха единствения файл извън обхвата на assistant промяната).
cf78482 to
edb55f7
Compare
…рифт-пазач) Речникът, който моделът чете (describe-schema.ts), дрифтна тихо веднъж: amendments.contract_id никога не е съществувала, а parties.role — също, и двете се откриха само на око при ревюто на midt-bg#321. Всяка фантомна колона там влиза във всеки prompt, а полученият SQL пада в runtime с грешка, която моделът „поправя" с ново налучкване. Тестът прилага ВСИЧКИ packages/db/migrations/*.sql поред върху временна sqlite3 база (same harness като packages/db/src/migrations.test.ts) и проверява: - всяка таблица от речника съществува; - всеки водещ идентификатор от всеки columns низ е реална колона (pragma_table_info; скобените бележки със запетаи/кавички се игнорират); - всяка →таблица референция е реална таблица; - всяка канонична заявка КОМПИЛИРА срещу схемата (EXPLAIN) — тях моделът копира дословно като отправна точка; - негативни контроли: историческият дрифт ('id, contract_id→contracts, …') се хваща, а parser-ът не се лъже от бележки в скоби. Проверено срещу main-версията на речника: пазачът докладва точно amendments.contract_id и parties.role. Понеже поправката им живее в midt-bg#321, клонът е стакнат върху него (мърдж ред: midt-bg#321 → този). Тестът живее в apps/web/test/ и е включен в tsconfig.node.json (Node типове за child_process), за да не се наливат Node globals в Workers кода на приложението; vitest include покрива test/**.
Част от #318 — наблюдаемостта, върху която стъпва предстоящото премерване на флора. Самата рекалибрация на
MIN_SCHEMA_SCORE/topKостава за втори PR, след като логовете съберат реални данни (issue-то остава отворено).Какво
retrieveSchemaContextподаваRetrievalStatsпрез опционаленonStatshook — три брояча, за да са различими двата класа повреди с противоположни поправки:matched— сурови namespace-скоупнати съвпадения (0 = празен/неиндексиран namespace или embed без вектор);aboveFloor— над релевантния флор (matched > aboveFloor= флорът реже);kept— реално стигнали до prompt-а (aboveFloor > kept= счупенmetadata.textконтракт, бъг в индексирането, НЕ флор).workers/request-log.ts— агрегируеми полета, само броячи, никога текста на въпроса) и вече не поглъща тихо грешката при retrieval (console.errorсамо с message — суров error обект може да ехне въпроса).topK/minScoreстанаха opts обект (RetrieveOptions).kept=0приmatched>0е точно тихият fallback към пълния речник, който досега беше неразличим от работещ RAG.Гаранции (от ревюто)
onStatsсе вика в try/catch — хвърлящ metrics sink не може да струва на хода неговите чънкове (инвариантът на request-log.ts); закован с тест.!vec— embed без използваем вектор) докладва, с нули — иначе е неразличим от „статистиката не е вързана"; тест.MIN_SCHEMA_SCORE ± ε(рекалибрацията няма да го прекатури) и затвърждава и върнатите чънкове, не само числата.Стак
Стъпва върху #320 (→ #319 → #223). За ревю са последните 2 комита (
feat+ review fixes). Ред на мърдж: #223 → #319 → #320 → този PR.Проверено
tsc -bчист, пълният пакет на apps/web: 510/510 теста, покритие над baseline (91.24 lines / 83.19 branches), Prettier чист.