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/**.
…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.
…ема пътя Без флор, щом 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 за чистене (бележки от ревюто).
…та да не зависи от стойността на флора) Този PR въвежда entity флора с Number.isFinite точно за да не пропусне scoreless match при minScore = 0 — но остави схема пътя на (m.score ?? 0), т.е. асиметрия, въведена в същия PR. При подразбиращия се 0.35 двата се държат еднакво, но извикване с minScore = 0 би пропуснало match без score като „контекст". Изравнено; тест точно за minScore = 0 (негативен контрол: връщането на ?? 0 го чупи). Бележка от ревюто на @ydimitrof.
…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).
Ранното връщане при `texts.length === 0` е това, което прави вярно „адаптерът никога не се вика с празен вход" за ВСЕКИ извикващ — не само за днешните три. Досега контрактът беше негласен (споменат само в bindings.ts); сега е записан на самото място, което го гарантира, с указание да не се мести под run(). Тестът в rag.test.ts вече закова, че моделът не се вика за []. (бележка от ревюто на midt-bg#320, ydimitrof)
…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 се вика два пъти (втория път с обекта).
Три дупки от ревюто на клона по gate-овете на отчета: - Прозата: голият суфикс -илион хващаше „павилион(и)" — рутинен предмет на поръчка — и отхвърляше легитимно заглавие като несвързано число, което моделът не може да пренапише. Суфиксите вече са котвени към числителните представки (м/б/тр/квадр/квинт/секст/септ/окт/нон/дец) — затворено нагоре до 10^33, „Илион" и „билярд" вече не флагват, „милионер" още (безопасната посока). - emit-report-schema: `sub` на facts не се проверяваше по тип — число минаваше shape-а и хвърляше TypeError в bindReport (непрозрачна tool грешка към модела вместо retryable съобщение, и несканирана цифра). `align: null`/`link: null` („не е зададено" на модела) вече се приемат като отсъстващи, вместо да обръщат иначе валиден отчет в retry. Трикратният copy-paste на cap guard-а е един cappedArray helper със същите съобщения (диференциално проверени). - bindReport: `link` се пресъздава от двете си известни полета, а не се копира по референция — допълнителен ключ вътре (href) иначе влизаше в замразения отчет въпреки „само известни полета стигат до рендера". Тестове за всяко; негативен контрол: новите тестове падат срещу старите файлове. JSONB близнаците в SQL денилиста (jsonb_group_*), първоначално част от този комит, са пренесени в основата на стека (midt-bg#223), където денилистът се въвежда.
…агва само след числително Две находки от втория преглед на клона: - 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 теста падат.
…tream грешки - „три трлн лева" се промъкваше през целия prose gate: няма цифра (за \d…-шаблона), няма пълнословен -илион стем и няма млн/млрд. Добавено към същия numeral+абревиатура клон. Само реално използваните в български финансов текст съкращения — измислено „квдрлн" би бил шаблон, който никой не пише, а пълната дума вече се лови от стема. - Дедупът на stream грешките ползваше WeakSet, т.е. работеше само за обектни грешки; примитив (низ/число), видян и от двата onError hook-а, се логваше двойно. Примитивите вече се дедупват по вече-редактирания текст, а обектите остават по идентичност (две различни грешки с еднакъв текст заслужават по ред). Логиката е изнесена в makeStreamErrorLogger, за да е тествана без реален стрийм — 4 теста, вкл. редакцията. Бележки от ревюто на @ydimitrof по midt-bg#321.
…еният списък числителни течеше Клонът „числително + абревиатура" пазеше със ЗАТВОРЕН списък кардинални числителни. Обикновена българска финансова проза минаваше покрай него и замразяваше необвързана сума в публичния отчет: „два и половина млрд. лева" (думата пред единицата е „половина"), „двайсет млн.", „трийсет млрд.", „стотина млн.", „десетина млн.", „четвърт млрд.", „дузина млн.", „няколкостотин млн." — всички връщаха ok:true през bindReport. Точно „12 млрд."-векторът, за който gate-ът съществува, с изписано число. Правилото вече е обърнато: абревиатурата флагва след ВСЯКА дума, освен след предлог за мерна единица (в/във/на/по/от/до/за/към/при/с/със/и/или — „Стойност в млн. лв.", „изразени във млрд."). Гол остава само след пунктуация/начало на ред — „Стойност (млн. €)", „Похарчено, млрд. лв." — стилът на собствените ни колони. „Стойност млн. €" (съществително директно пред единицата) вече флагва: без пълен речник на числителните е неразличимо от „стотина млн.", а безопасната посока е over-flag (моделът слага единицата в скоби). „трлн" влиза и в цифровия клон — „12 трлн. лева" / „1,5 трлн" (формата, която моделът най-често пише) минаваше целия gate, макар „три трлн лева" да се хващаше (ревю на ydimitrof по абревиатурния клон). Тестове: десетте формулировки + bindReport с текстов блок; предлозните и скобените форми остават чисти; негативен контрол: и трите променени/нови теста падат срещу стария regex. (независим преглед след ревюто на midt-bg#321)
…з pre-cap-а Фикстурата беше 18 105 UTF-16 единици при праг MAX_RAW_CHARS = 19 200, така че preCap() връщаше суровото съобщение непроменено и тестът „Pre-cap regression guard" никога не влизаше в пътя, който коментарът му твърди, че пази — регресия в реда pre-cap/collapse/redact щеше да остане зелена. Фикстурата вече е ~21 100 единици, MAX_RAW_CHARS е експортнат и тестът заковава `question.length > MAX_RAW_CHARS`, за да не може да се смъкне тихо под прага отново. Очакваният изход остава същият: една редактирана линия „err: «редактирано»". (независим преглед след ревюто на midt-bg#321)
…i cap-а, ok пътя на emit_report и отказите на route-а Клонове от кода в този PR, които суитът не докосваше след пребазирането върху новия coverage baseline на main (midt-bg#254): - errorTag: RetryError, който обвива НЕ-APICallError — тагът е само причината, без статус; - log-safety: needle между MIN_REDACT_CHARS и REDACT_WINDOW се редактира по точен match, а съобщение с повече UTF-16 единици от cap-а, но по-малко кодови точки, не се реже; - agent wiring: валиден emit_report минава по ok клона и връща вързаната справка; - route door: и петте отказа на входа (обявен/реален над-cap body, невалиден JSON, нула ходове след филтъра на ролите, един гигантски ход) връщат 4xx, без да стигат до модела. Content-Length се подава през незащитен Headers обект, защото истинският Request го изпуска.
…ната към типа
Изричното изграждане на колоната в bindReport (вместо `{ ...c }`) копира
фиксиран списък полета. Ако EmitTableColumn някога получи ново незадължително
поле, старият код би го изпуснал тихо — без TS грешка (ревю на midt-bg#330,
ydimitrof). REBUILT_COLUMN_KEYS/REBUILT_LINK_KEYS са `Record<keyof …, true>`
обекти: липсващо или излишно свойство е компилационна грешка, така че
списъкът не може да изостане от типа. Тестът закова и runtime страната —
напълно зададена колона излиза с точно тези ключове, а непознат ключ (вкл.
`href` вътре в `link`) не оцелява. Негативен контрол: `width?: number`,
добавено към интерфейса, чупи `tsc -b` на пина.
…рифт-пазач) Речникът, който моделът чете (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/**.
42e1362 to
d9e75f2
Compare
Част от #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 чист.