Skip to content

fix(assistant): типизирани AI/Vectorize биндинги — без 'as unknown as' на route границата - #320

Open
nedda76 wants to merge 28 commits into
midt-bg:mainfrom
nedda76:fix/assistant-typed-bindings
Open

fix(assistant): типизирани AI/Vectorize биндинги — без 'as unknown as' на route границата#320
nedda76 wants to merge 28 commits into
midt-bg:mainfrom
nedda76:fix/assistant-typed-bindings

Conversation

@nedda76

@nedda76 nedda76 commented Aug 18, 2026

Copy link
Copy Markdown
Collaborator

Closes #316

Какво

Двойните assertions (env.AI as unknown as EmbeddingRunner, env.VECTORIZE as unknown as VectorIndex) в routes/assistant.chat.tsx изчезват:

  • VECTORIZE се присвоява директно: VectorRecord.metadata е стеснен до стойностите, които Vectorize приема (VectorMetadataValue), а неизползваемият метадата filter член отпадна от интерфейса. Присвояването е компилационното доказателство — дрейф между rag.ts и worker-configuration.d.ts чупи tsc, не продукцията. Негативен контрол: връщането на стария filter?: Record<string, unknown> член дава TS2322 точно на присвояването.
  • AI не може да удовлетвори EmbeddingRunner структурно (Ai.run() е generic per-model и връща output UNION), затова минава през единствения санкциониран мостembeddingRunnerFor() в новия bindings.ts, който вика реалния @cf/baai/bge-m3 overload, също проверен от компилатора.

Кастовете към AgentEnv остават: BGGPT_API_KEY е secret и не присъства в генерирания Env — отделен въпрос.

От ревюто (второ комитче)

  • EmbeddingRunner.run взима model: typeof EMBED_MODEL (литерала) и адаптерът го препраща — втори, различен модел би бил компилационна грешка, не тихо embed-ване с грешния модел.
  • Адаптерът е изнесен в bindings.ts и unit тестван (вкл. не-embedding форми на отговора).
  • При неочаквана форма адаптерът хвърля именувана грешка (само ключовете, без payload) вместо да връща [], което се четеше като „провайдърът не embed-на нищо".
  • README provisioning gate-ът вика indexSchemaCorpus през embeddingRunnerFor — голият env.AI там вече не typecheck-ва.

Стак

Стъпва върху #319 (→ #223). За ревю са последните 2 комита (eb05524, 78d36fb). Ред на мърдж: #223#319 → този PR.

Проверено

tsc -b чист (вкл. негативния контрол по-горе), 185/185 теста на assistant пакета, Prettier чист.

@ydimitrof ydimitrof left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Обобщение на ревюто

Фаза 0 — Security-Critical Scan: CLEAN. Няма хардкоднати тайни (BGGPT_API_KEY се подава през wrangler secret put, никога не се комитва). Няма нови URL-и извън whitelist. Няма backdoor/обфускация/code injection. Промяната в osv-scanner.toml е suppression, но е добре обоснована (sharp е транзитивна, само-dev зависимост през miniflare, не влиза в деплойнатия Worker; има ignoreUntil и ясно условие за премахване). Нови зависимости не се добавят. → Продължавам към същинското ревю.

Оценка по измерения

Сигурност (агентско ниво) — отлично. Ядрото на PR-а всъщност повишава сигурността:

  • Премахва as unknown as на граница route↔RAG (#316) — типов контракт, който tsc проверява, вместо сляп каст.
  • sql-guard.ts разширява denylist-а с string-building агрегати (group_concat/string_agg/json_group_array/json_group_object) — реален клас memory-amplification (OOM на изолата преди capRows). Правилно уловен и string_agg синонимът за модерен SQLite на D1.
  • emit-report-schema.ts: горни граници за масиви (MAX_BLOCKS/ITEMS/COLUMNS) + short-circuit преди сканиране на oversize структура; align whitelist (left|right) спира out-of-enum стойност да достигне рендерер, който я интерполира в атрибут.
  • report-schema.ts: експлицитно изграждане на колоните (без spread) спира пренасяне на непроверени model-supplied ключове към рендерера — добра defense-in-depth.
  • Диагностиките са „keys-only" (не логват съдържанието на ембеднатия вход) — правилно за да не изтича потребителски текст.

Архитектура — отлично. bindings.ts е единственият модул, който знае и двете страни на границата; структурните типове в rag.ts остават deploy-независими и unit-тестируеми. Разделянето hardTraps() (безусловно) vs. RAG-извлечени chunk-ове поправя реален дефект: RAG-ход, който е бил по-слабо ограничен от no-RAG fallback-а (пропускал е SUM(amount)). renderTraps() премахва дублирането между describeSchema() и hard-traps пътя.

Производителност — без регресии. Native namespace вместо metadata filter (не изисква provisioned metadata index, изключва stale кохорти преди topK). Relevance floor (MIN_SCHEMA_SCORE) предотвратява инжектиране на off-topic chunk-ове вместо пълния речник. Версионираните namespace-и позволяват rollback на Worker-а без счупване на RAG.

Тестове — много силни. Добавени/разширени тестове покриват: adapter forward + два error-shape случая, versioned namespace/ids, relevance floor (вкл. scoreless-match defensive пътища), cap short-circuit, align whitelist, exactly-once рендиране на trap-овете през реалния write→read seam, суфиксното matching на магнитуди, новите SQL агрегати. Тестовете са смислени (не тривиални), проверяват границите и regression пътищата.

Документация — отлично. README е обстойно обновен: ре-индексиране, „WHEN TO BUMP" правило, разграничение схема-корпус (append-only) vs. entity-корпус (data-derived, нужен reconciliation/delete път).

CLAUDE.md съответствие

Няма частична имплементация, TODO-симплификации, дублиран или мъртъв код. Именуването е консистентно. Разделянето на отговорности е чисто. Няма resource leaks.

Незначителни наблюдения (не блокиращи — виж inline)

  1. sql-guard.ts: denylist-ът fail-closed отхвърля и стрингови литерали, съдържащи имената на функциите (напр. WHERE name = 'group_concat(x)'). Това е предсъществуващо поведение (printf/format), безопасната посока е, и коментарът вече насочва към positive allowlist като трайно решение.
  2. report-schema.ts: суфиксният подход не покрива съкращенията млрд./млн. без придружаваща валутна дума или 5-цифрено число — потенциален (маргинален) false-negative.

Препоръка: Няма блокиращи проблеми. Кодът е за мърдж след потвърждаване на пълния тестов пакет и coverage в CI (не можах да ги изпълня в тази среда) и предварителното осигуряване на Cloudflare ресурсите (Vectorize sigma-assistant 1024/cosine, R2 sigma-reports, BGGPT_API_KEY, ре-индекс на схема-корпуса) преди deploy, както е описано в README.

Comment thread apps/web/app/lib/assistant/sql-guard.ts Outdated
Comment thread apps/web/app/lib/assistant/report-schema.ts Outdated
Comment thread apps/web/app/lib/assistant/rag.ts
Comment thread apps/web/app/lib/assistant/system-prompt.ts
nedda76 added a commit to nedda76/sigma that referenced this pull request Aug 19, 2026
… gate-а

„Дванадесет млрд. лева" нямаше нито цифра (за \d…млрд шаблона), нито
пълнословен суфикс — изписано числително + абревиатура се промъкваше
покрай целия gate. млрд/млн влизат в стем шаблона (флагват и без цифра;
негативен контрол: тестът пада без промяната). Остатъкът „хил." без
цифра остава приет — хилядите не са defamation-мащабният вектор
(бележка от ревюто на midt-bg#320).
@nedda76
nedda76 force-pushed the fix/assistant-typed-bindings branch from 78d36fb to e167365 Compare August 19, 2026 17:10

@ydimitrof ydimitrof left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ревю на PR: типизирани AI/Vectorize биндинги (без as unknown as)

Обобщение

Много силен, добре обоснован PR. Основната цел — премахване на as unknown as на границата на route-а (assistant.chat.tsx) и въвеждане на един-единствен санкциониран мост (bindings.tsembeddingRunnerFor) — е постигната чисто и е покрита с прицелни тестове. Съпътстващите промени (versioned native namespaces в Vectorize, relevance floor при retrieval, capове на масивите в emit_report, разширяване на SQL denylist-а и prose-number gate-а) са последователни, добре документирани и всяка носи регресионен тест. Разделението на отговорностите е спазено: bindings.ts е единственият модул, който познава двете страни на типовете.

Фаза 0 — Security-critical scan: CLEAN

  • Hardcoded secrets: няма. BGGPT_API_KEY минава през wrangler secret put, никога не се комитва; документацията изрично го подчертава.
  • URL промени: няма нови външни URL-и. Единствените нови идентификатори са модел-литерал (@cf/baai/bge-m3) и имена на Cloudflare ресурси (sigma-assistant, sigma-reports).
  • Malicious patterns: няма backdoor/обфускация/eval.
  • Зависимости: osv-scanner.toml добавя ignore само за sharp (транзитивна, dev-only през miniflare) с ясна обосновка и ignoreUntil. Приемливо.
  • Логове: адаптерът съзнателно логва само ключове (KEYS ONLY), за да не изтича потребителски текст — добра практика.

Силни страни

  • Типова безопасност на route границата: env.VECTORIZE се присвоява без каст (compile-time доказателство за структурна съвместимост), а env.AI минава през embeddingRunnerFor, който извиква реалния per-model overload. Точно това, което issue #316 иска.
  • Sanitizing на emit_report: експлицитното изграждане на колоните (без spread) спира пренасянето на непознати model-подадени полета към renderer-а — реално подобрение на сигурността срещу injection в атрибути/стил, подсилено от isAlign whitelist-а.
  • SQL guard: добавянето на string-building агрегатите (group_concat/string_agg/json_group_*) затваря реален memory-amplification вектор; коментарът честно признава, че denylist-ът е catch-up игра и allowlist е трайният фикс.
  • Prose-number gate: преминаването от явен списък към суфиксите -илион/-илиард затваря реда нагоре (квинтилион/секстилион) — добро решение, съзнателно приема over-flagging в правилната посока.
  • RAG namespaces + relevance floor: native namespace вместо metadata filter (който изисква непровизиран metadata index) и floor-ът срещу off-topic topK са коректни; fallback-ът към пълния статичен речник е безопасният изход.

Забележки (ниска тежест)

Виж inline коментарите. Няма блокиращи проблеми.

Проверимост

Тестовете и typecheck-ът не са изпълнени в тази среда (няма достъп до repo build/CI). Финалната препоръка е при условие за зелен CI: pnpm --filter web typecheck → 0 и целият тестов пакет минава, съгласно README.

Съответствие с CLAUDE.md

  • NO PARTIAL IMPLEMENTATION / TODO: спазено — недовършените части (entity indexer, eop_fetch само брой) са ясно документирани като „Какво остава", не са скрити TODO-та.
  • NO CODE DUPLICATION: renderTraps() е извлечен и споделен между describeSchema и hardTraps, точно за да не дрейфа.
  • COMPREHENSIVE / NO CHEATER TESTS: тестовете са смислени (проверяват форма на namespace, floor поведение, cap short-circuit, exactly-once на trap-овете през реалния write→read seam).
  • CONSISTENT NAMING / SEPARATION OF CONCERNS: спазено.

Препоръка

APPROVE (при зелен CI). Качество: ~9.4/10.

Comment thread apps/web/app/lib/assistant/bindings.ts Outdated
Comment thread apps/web/app/lib/assistant/rag.ts

@ydimitrof ydimitrof left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ревю на PR: типизирани AI/Vectorize биндинги (без as unknown as)

Фаза 0 — Сигурност (сканиране): ЧИСТО (с една бележка)

  • Няма hardcoded тайни. BGGPT_API_KEY се подава през wrangler secret put и изрично не се комитва — коректно.
  • Няма нови/променени URL адреси, няма backdoor/обфускация/code-injection модели.
  • Промяна по зависимости — само подтискане в osv-scanner.toml: добавени са игнори за sharp 0.34.5 (libvips CVE-та). Обосновката е валидна (транзитивна, само-dev зависимост през miniflare, не влиза в деплойнатия Worker, няма in-range upstream фикс) и има ignoreUntil дата за ре-визия. Приемливо, но флагвам за проследяване — това остава подтисната уязвимост до 2026-10-22.

Обща оценка

Много силен, добре обоснован PR. Основната цел — премахване на as unknown as на границата на route-а — е постигната чисто:

  • env.VECTORIZE вече се присвоява структурно към VectorIndex (compile-time доказателство), а env.AI минава през единствения санкциониран мост embeddingRunnerFor(), който форва̀рдва EMBED_MODEL литерала през реалния @cf/baai/bge-m3 overload.
  • Адаптерът в bindings.ts не губи диагностиката, която сляп каст губеше (именувана грешка по форма, keys-only — без изтичане на потребителски текст в логовете). Отлично.

Тестове (силна страна)

  • Новите bindings.test.ts пинват поведението на адаптера (форуард, невалидна форма, липсващи ключове, празен data масив за непразен вход).
  • rag.test.ts покрива версионирания native namespace (schema-v2/entity-v1), релевантния праг, scoreless match-овете и отсъствието на metadata filter.
  • Композиционният тест в system-prompt.test.ts минава през реалния write→read seam (indexSchemaCorpus → retrieveSchemaContext → buildSystemPrompt) и гарантира, че всеки trap се рендира точно веднъж — това хваща регресията с двойния рендер.
  • sql-guard.test.ts покрива quoted-identifier bypass и string-building агрегатите.

Архитектура / CLAUDE.md

  • renderTraps() премахва дублирането между describeSchema() и hardTraps() — DRY спазено.
  • Разделянето на контрактите (VectorIndex е assignable от VectorizeIndex; EmbeddingRunner — не) е ясно документирано.
  • hardTraps() инжектира императивните капани безусловно — RAG вече само добавя релевантни таблици/заявки, така че RAG-ход никога не е по-слабо ограничен от fallback-а. Коректна поправка на реален дефект.

Точки за внимание (не блокиращи)

  1. Ред на деплой (операционен риск, документиран): PR добавя изисквания за bindings — Vectorize индекс sigma-assistant (1024/cosine), R2 sigma-reports, secret BGGPT_API_KEY. Ако деплой стане преди тяхното създаване, wrangler deploy пада и блокира CD на целия екип. Документирано подробно в README; уверете се, че CD-то го прилага преди мърдж.
  2. Namespace миграция: заявките сега сочат schema-v2/entity-v1. Среда без ре-индекс връща 0 чънка и пада към пълния статичен речник — безопасно, но без RAG grounding, докато indexSchemaCorpus не се пусне отново. Проследете ре-индекса като част от deploy runbook-а.
  3. Малка бележка по semanticSearch (вж. inline).

Заключение

Няма блокиращи проблеми по код или сигурност. Не давам автоматичен APPROVE, защото agent loop-ът и route-ът остават runtime-непроверени (няма BGGPT_API_KEY/облачни bindings в средата — признато в README), а деплой-редът е външно условие. Verdict: COMMENT — след потвърден ре-индекс и осигурени bindings PR-ът е готов за мърдж.

Comment thread apps/web/app/lib/assistant/rag.ts Outdated
Comment thread apps/web/app/lib/assistant/sql-guard.ts Outdated
@nedda76
nedda76 force-pushed the fix/assistant-typed-bindings branch from 861fd43 to 6623c91 Compare August 20, 2026 07:01
nedda76 added a commit to nedda76/sigma that referenced this pull request Aug 21, 2026
… gate-а

„Дванадесет млрд. лева" нямаше нито цифра (за \d…млрд шаблона), нито
пълнословен суфикс — изписано числително + абревиатура се промъкваше
покрай целия gate. млрд/млн влизат в стем шаблона (флагват и без цифра;
негативен контрол: тестът пада без промяната). Остатъкът „хил." без
цифра остава приет — хилядите не са defamation-мащабният вектор
(бележка от ревюто на midt-bg#320).
@nedda76
nedda76 force-pushed the fix/assistant-typed-bindings branch from 6623c91 to 5652681 Compare August 21, 2026 10:54
nedda76 added a commit to nedda76/sigma that referenced this pull request Aug 24, 2026
… gate-а

„Дванадесет млрд. лева" нямаше нито цифра (за \d…млрд шаблона), нито
пълнословен суфикс — изписано числително + абревиатура се промъкваше
покрай целия gate. млрд/млн влизат в стем шаблона (флагват и без цифра;
негативен контрол: тестът пада без промяната). Остатъкът „хил." без
цифра остава приет — хилядите не са defamation-мащабният вектор
(бележка от ревюто на midt-bg#320).
nedda76 added a commit to nedda76/sigma that referenced this pull request Aug 24, 2026
…е като успех

[] е truthy — проверка само за присъствие връщаше { data: [] } за
непразен вход и embed() после обвиняваше '0 embeddings' вместо реалната
причина: провайдър, отговорил с празен batch. Празният масив вече е
именуван отделен случай в грешката на адаптера (+ тест; бележка от
ревюто на midt-bg#320).
@nedda76
nedda76 force-pushed the fix/assistant-typed-bindings branch from 5652681 to c454c04 Compare August 24, 2026 12:48
nedda76 added a commit to nedda76/sigma that referenced this pull request Aug 25, 2026
… gate-а

„Дванадесет млрд. лева" нямаше нито цифра (за \d…млрд шаблона), нито
пълнословен суфикс — изписано числително + абревиатура се промъкваше
покрай целия gate. млрд/млн влизат в стем шаблона (флагват и без цифра;
негативен контрол: тестът пада без промяната). Остатъкът „хил." без
цифра остава приет — хилядите не са defamation-мащабният вектор
(бележка от ревюто на midt-bg#320).
nedda76 added a commit to nedda76/sigma that referenced this pull request Aug 25, 2026
…е като успех

[] е truthy — проверка само за присъствие връщаше { data: [] } за
непразен вход и embed() после обвиняваше '0 embeddings' вместо реалната
причина: провайдър, отговорил с празен batch. Празният масив вече е
именуван отделен случай в грешката на адаптера (+ тест; бележка от
ревюто на midt-bg#320).
@nedda76
nedda76 force-pushed the fix/assistant-typed-bindings branch from c454c04 to c37b1e9 Compare August 25, 2026 18:29
nedda76 added a commit to nedda76/sigma that referenced this pull request Aug 26, 2026
… gate-а

„Дванадесет млрд. лева" нямаше нито цифра (за \d…млрд шаблона), нито
пълнословен суфикс — изписано числително + абревиатура се промъкваше
покрай целия gate. млрд/млн влизат в стем шаблона (флагват и без цифра;
негативен контрол: тестът пада без промяната). Остатъкът „хил." без
цифра остава приет — хилядите не са defamation-мащабният вектор
(бележка от ревюто на midt-bg#320).
nedda76 added a commit to nedda76/sigma that referenced this pull request Aug 26, 2026
…е като успех

[] е truthy — проверка само за присъствие връщаше { data: [] } за
непразен вход и embed() после обвиняваше '0 embeddings' вместо реалната
причина: провайдър, отговорил с празен batch. Празният масив вече е
именуван отделен случай в грешката на адаптера (+ тест; бележка от
ревюто на midt-bg#320).
@nedda76
nedda76 force-pushed the fix/assistant-typed-bindings branch 2 times, most recently from 1e05ef9 to 6dcd8a8 Compare August 30, 2026 13:29

@ydimitrof ydimitrof left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Обобщение

Много добре изпипан PR. Основната цел — премахване на as unknown as на границата route→lib и въвеждане на типизирани биндинги (issue #316) — е постигната чисто: VectorIndex е структурно съвместим с VectorizeIndex (присвояването в assistant.chat.tsx е compile-time доказателството), а Ai минава през единствения санкциониран мост embeddingRunnerFor. Тестовото покритие е образцово (адаптер, relevance floor, cap-ове, quoted-identifier bypass).

Прегледах и за враждебен код: няма exfiltration, нови мрежови повиквания, обфускирани payload-и или промени по auth/workflow файлове. osv-scanner.toml игнорите за sharp са обосновани (transitive, dev-only чрез miniflare, извън деплойнатия Worker) и с ignoreUntil дата. Няма prompt-injection в дифа.

Отбелязвам няколко забележки, всичките незадължителни — нищо блокиращо.

Забележки (незадължителни)

  1. rag.ts — асиметрия между retrieveSchemaContext и semanticSearch за scoreless match. retrieveSchemaContext използва (m.score ?? 0) >= minScore, докато semanticSearch съзнателно ползва Number.isFinite(m.score) && m.score >= minScore, точно за да не пропусне безскоров match при minScore = 0. minScore е публичен параметър по подразбиране MIN_SCHEMA_SCORE; ако бъдещ викащ подаде 0 (както прави entity-тестът), схема-пътят ще пропусне безскоров запис през пода 0 (0 >= 0), а entity-пътят — не. За консистентност и да не се появи този клас грешка по-късно, изравнете schema-филтъра към Number.isFinite(m.score) && m.score >= minScore.

  2. sql-guard.ts — denylist срещу нови алиаси е догонваща игра (както сами отбелязвате в коментара). Класът покрива quoted-identifier bypass-а и aggregate string-builder-ите добре, но позитивен allowlist на функции остава по-устойчивото решение; добре е, че е проследен отделно. Няма нужда от промяна тук.

  3. bindings.ts — предположението „embed() никога не вика адаптера с празен вход" е коректно за текущите три викащи (retrieveSchemaContext/semanticSearch/indexSchemaCorpus подават непразни масиви), но е негласен контракт. Ако някой добави нов път, който подаде празни texts, съобщението „празен data масив за непразен вход" ще подведе. Струва си кратък guard или коментар в самия embed.

Одобрявам — промените са коректни, добре тествани и подобряват сигурността на retrieval границата.

@nedda76
nedda76 force-pushed the fix/assistant-typed-bindings branch 2 times, most recently from 06b8d57 to 89ea450 Compare September 2, 2026 13:30
@nedda76

nedda76 commented Sep 2, 2026

Copy link
Copy Markdown
Collaborator Author

Благодаря за прегледа — по трите незадължителни бележки:

  1. Асиметрията ?? 0 / Number.isFinite в rag.ts — изравнена още в fix(assistant): semantic_search през native Vectorize namespace вместо metadata филтър #319 (4bb6004, в основата на този клон): схема пътят е Number.isFinite(m.score) && m.score >= minScore, с тест за minScore = 0 (истинска нула остава hit, scoreless отпада).
  2. Денилистът като догонваща игра — съгласна. Междувременно в fix(assistant): integrity, grounding & guard fixes from the PR #79 review #223 денилистът стана една дефиниция, приложена и на AST ниво върху резолвнатото име на извикването, така че лексикален трик не може да го заобиколи (детайли в коментара там). Allowlist-ът (run_sql: durable memory-amplification защита (allowlist на функции + cell-size cap преди capRows) #227) остава трайното решение.
  3. Негласният контракт „embed() никога не вика адаптера с празен вход" — записан на самото място, което го гарантира (89ea450): ранното връщане при texts.length === 0 в embed() вече носи коментар, че то Е контрактът на адаптера за ВСЕКИ извикващ (не само днешните три) и не бива да се мести под run(). Тестът в rag.test.ts вече заковаваше, че моделът не се вика за [] — guard имаше, липсваше изричното назоваване.

Клонът е пребазиран върху обновените #223/#319.

@nedda76
nedda76 force-pushed the fix/assistant-typed-bindings branch from 89ea450 to 55c0b1f Compare September 2, 2026 17:33
@nedda76

nedda76 commented Sep 2, 2026

Copy link
Copy Markdown
Collaborator Author

Пребазирано върху текущия main (8529c12); собствената делта на PR-а е непроменена.

@lyubomir-bozhinov lyubomir-bozhinov left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Прегледах собствената делта на #320 (4 комита над #319, при HEAD 55c0b1f4) — типизирани AI/Vectorize биндинги (#316).

Чисто (потвърдено):

  • Типовете отговарят на runtime shape-а — няма as unknown as лъжа. Vectorize четенията (.matches, m.score, m.metadata) са валидни (VectorizeMatch.score е required); AI адаптерът (bindings.ts:26-35) гардва реалната bge-m3 форма: if ('data' in out && Array.isArray(out.data) && out.data.length > 0) иначе throw с диагностична форма („празен data масив за непразен вход") — типът НЕ обещава фалшиво data (optional е в runtime union-а). Останалите env as unknown as са само за BGGPT_API_KEY (secret извън generated Env), извън обхвата на #316.
  • Empty-data throw-ът е коректен И хванат навсякъде: embed() early-return [] при празен вход; реален provider empty-batch → throw, хванат в retrieveSchemaContext try/catch (assistant.chat.tsx:119-122 → fallback към статичния речник) и в semanticSearch try/catch (tools.ts) → friendly message. Няма нов crash surface.
  • #319 гаранциите — непокътнати след type-рефактора: и двата query-та с hardcoded namespace (SCHEMA_NS/ENTITY_NS, rag.ts:142/176/234), floor-ът Number.isFinite(m.score) && m.score >= minScore симетричен (:187/241).
  • Тестовете (bindings.test.ts) са поведенчески: passthrough, keys-only named error, „празен data масив" rejection — мапнати към реални bge-m3 union членове.

Merge-ред: стек #223#319#320. Нямам блокери по собствената делта.

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).
group_concat / json_group_array / json_group_object collapse an entire
full-table scan into one huge cell that materialises in Worker memory before
capRows can measure it (and capRows keeps the first row whole) — the same
memory-amplification class already blocked for printf/format/randomblob, one
level up. Add them to the scalar blocklist.
- Prose number-gate missed трилион/билион/квадрилион: '3 трилиона лева' slipped
  the whole gate (the digit can't reach 'лева' across the Cyrillic word), an
  unbound order-up figure on a public report — the '12 млрд.' vector one
  magnitude higher. Add them to the spelled-magnitude stem.
- Validate the optional column align against a left|right whitelist, and build
  resolved table columns explicitly instead of spreading the model object, so no
  unknown/unvalidated property reaches the renderer.
- Cap model-emitted array lengths (blocks, items, columns) in validateEmitShape.
…h prompt paths

renderTraps() now owns the numbered-list rendering that describeSchema (full
dictionary) and the RAG hard-traps block duplicated, so the two paths cannot
drift, and the full-dictionary heading is harmonised to match the RAG block
("Задължителни правила за данните"). No behaviour change — string assembly only.
retrieveSchemaContext relied on `m.score` always being numeric. If an index
backend ever returns a match without a `score`, the comparison was falsy and the
chunk was dropped — the correct, safe outcome, but only incidentally. Make it
explicit with `(m.score ?? 0) >= minScore` and a comment so a future refactor
can't strip the guard, and cover it with a test. Addresses the review note on
rag.ts robustness (ydimitrof).
…vers квинтилион+)

The prose-number gate listed magnitudes explicitly and stopped at квадрилион, so
"3 квинтилиона лева" slipped. Match the shared suffixes instead — милион⊃"илион",
милиард⊃"илиард" — which covers the whole family (милион…секстилион…, милиард…)
and closes the row upward for good rather than chasing an endless list. Addresses
the review note on report-schema.ts (ydimitrof).
… the SQL guard

string_agg(X, sep) is the official SQLite 3.44 synonym of group_concat and reaches
the same code path on D1's modern SQLite, so it bypassed the scalar/aggregate
denylist and achieved the same memory amplification (whole scan into one cell
before capRows) the guard just closed for group_concat. Add it to the regex and
the adversarial test. Addresses the review note on sql-guard.ts (ydimitrof).
An over-cap blocks/items/columns array is exactly the unbounded structure
the ceilings guard against, yet validateEmitShape recorded the length error
and then walked the whole array anyway — doing the very scan the cap exists
to refuse. Return before the per-block scan on oversized blocks, and skip the
per-element scan on oversized items/columns. Behaviour is unchanged for valid
reports (ok:false either way); this only stops the wasted walk. Test asserts a
single cap error with no per-element errors, proving the array is not scanned.

Addresses lyubomir-bozhinov's review note on PR midt-bg#223.
…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)
…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 — не
изисква обработка (бележка от ревюто).
…наваха и двата guard-а

Денилистът изброяваше json_group_array/json_group_object, но не и JSONB
вариантите им jsonb_group_array/jsonb_group_object (SQLite ≥3.45, в build-а
на workerd). Буквалът `json_group_array` не е подниз на `jsonb_group_array`,
така че регексът не хващаше, а AST guard-ът гледа само FROM-източници, LIMIT
и дублирани колони — не функциите в SELECT-листата. `SELECT jsonb_group_array(
name) FROM bidders` минаваше и двата слоя и колабираше цялата таблица в една
JSONB клетка ПРЕДИ capRows — точно класът memory-amplification, който
денилистът цели (midt-bg#227).

Регексът вече е `jsonb?_group_(?:array|object)` — покрива и двете форми,
включително цитираните идентификатори през същия quote клас. Тестът добавя
голия и цитирания JSONB вариант; негативен контрол: новите случаи падат срещу
стария регекс. Поправката живееше само на върха на стека (91d175c в midt-bg#321);
пренесена е в основата, където денилистът се въвежда (ревю на midt-bg#223,
lyubomir-bozhinov).
…истът пази и на AST ниво

Скенерите stripComments/splitStatements моделираха само '…' литерали. Един `'`
вътре в двойно-кавичен alias (`AS "x'y"`) ги обръщаше в „в низ" до края на
заявката: следващ `/**/` или `--` оцеляваше дословно, `group_concat/**/(x)`
минаваше функционалния regex (който допуска само whitespace преди скобата), а
SQLite чете коментара като whitespace и изпълнява агрегата. AST guard-ът не
гледаше имена на функции, така че и двата слоя пропускаха — включително
printf/randomblob и новите jsonb_group_*. Възпроизведено срещу sqlite3 3.51:
`SELECT 1 AS "x'y", group_concat/**/(subject, '') FROM tenders` се изпълнява.

- sql-guard.ts: и четирите форми на кавички на SQLite ('…', "…", `…`, […])
  са непрозрачни спанове и за двата скенера (удвоен затварящ знак = escape,
  `]` няма escape); незатворен спан тече до края и AST слоят фейлва CLOSED.
  Денилистът е ЕДНА дефиниция (DENIED_FUNCTION_NAME), споделена с AST слоя.
- sql-ast-guard.ts: обхожда парснатото дърво на всяка дълбочина (аргументи,
  WHERE, под-заявки, CTE тела) и отхвърля денилистваните имена по
  РЕЗОЛВНАТОТО име на извикването — коментари, кавички и регистър вече са
  премахнати от парсера, така че лексикален трик не може да скрие име.
  Непозната форма на име → fail closed. Формите са снети от реалния
  node-sql-parser 5.4 (aggr_func с низ; function с name.name[].value).

Тестове: шестте bypass формулировки падат на L1; L2 отхвърля същите подадени
ДИРЕКТНО (без L1), вкл. вложени в аргумент и в под-заявка; позитивен контрол
за обичайните скаларни/агрегатни функции; идентификатори с `--`, `/* */`,
`;` и удвоена кавичка остават данни. Негативен контрол: и трите нови теста
падат срещу стария код. (независим преглед след ревюто на midt-bg#223)
…спан и непозната форма на име

Двата пътя, по които новите скенер и AST проверка фейлват CLOSED, нямаха
тест: незатворен кавичен спан (тече до края на входа; `;` вътре не разделя,
но keyword блоклистът пак чете текста, а безобиден остатък пада на парсера)
и call node с форма на име, която callName не разпознава (никакъв SQL текст
не я произвежда от парсера — затова denyDeniedFunction е експортната и се
проверява с конструиран възел). Покрива и обхождането на масиви/вложени
обекти и резолването до lower-case име.
…инг литералите

Регексите на първия слой (ключови думи, pragma_, TVF, каталожни таблици,
функционалният денилист) вървяха върху стрипнатия SQL, в който стринг
литералите са дословни — така заявка, която само ТЪРСИ текст с име на функция
или ключова дума (`WHERE subject LIKE '%group_concat(%'`, `'%DROP TABLE%'`),
се отхвърляше фалшиво, и то само от този слой: AST слоят отказва единствено
реални извиквания (ревю на midt-bg#321, ydimitrof).

Проверките вече четат копие, в което всеки единично-кавичен литерал е сведен
до `''` (blankStringLiterals, върху същия quotedSpanEnd скенер). Кавичните
ИДЕНТИФИКАТОРИ ("…", `…`, […]) остават видими нарочно — SQLite резолва
`"group_concat"(x)` до вградената функция и името трябва да се види. Върнатото
изпълнимо изявление е истинското, с непокътнати литерали.

Тест: четирите LIKE/= форми минават и двата слоя с непроменен SQL; същото
име извън литерал (вкл. до литерал и в кавичена форма) остава отказано.
Негативен контрол: новият тест пада срещу стария код.

Единственото място, където единично-кавичен токен НЕ е данни, е позицията на
таблица: граматиката на SQLite има `nm ::= id | STRING`, така че
`FROM 'sqlite_master'` чете реалния каталог, а бланкирането би заслепило
каталожния/pragma_/TVF backstop за този правопис. Затова всеки кавичен токен
след FROM/JOIN (вкл. schema-квалифициран) се отказва изрично на L1 —
AST allowlist-ът го отказва и без това, но не бива да е единственият слой.
Имената на функции са само `id` (`'printf'(x)` е синтактична грешка), така че
функционалният регекс не губи нищо. Тест за шестте форми + позитивен контрол
за литерал, който сам съдържа „from 'x'".
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 за чистене (бележки от ревюто).
…та да не зависи от стойността на флора)

Този 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)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

assistant: типизирай AI/Vectorize биндингите вместо 'as unknown as' кастове на границата на route-а

3 participants