Skip to content

test: тестово покритие ≥95% за всички workspaces (надгражда #216) - #254

Merged
todorkolev merged 51 commits into
midt-bg:mainfrom
lyubomir-bozhinov:test/coverage-ratchet
Sep 2, 2026
Merged

todorkolev merged 51 commits into
midt-bg:mainfrom
lyubomir-bozhinov:test/coverage-ratchet

Conversation

@lyubomir-bozhinov

@lyubomir-bozhinov lyubomir-bozhinov commented Jul 17, 2026 •

Copy link
Copy Markdown
Collaborator

Надгражда coverage харнеса от #216 с реалните тестове и вдига ratchet прага на ≥96% клонове и ≥98.8% редове за всеки workspace (повечето на 100%). Само тестове — нулева промяна в продуктов код.

⚠️ Ред на сливане — зависи от #216

Този PR трябва да се слее след #216 (ydimitrof:feat/coverage-ratchet), който все още е отворен.

#216 въвежда самия харнес — scripts/check-coverage.mjs + check-coverage.test.mjs, споделения vitest.shared.ts, per-workspace vitest.config.ts, coverage-baseline.json (с началните, ниски прагове) и CI стъпката. Тук този харнес е vendor-нат едно към едно (двата скрипта са байтово идентични с #216), за да могат тестовете да се пишат и пускат срещу реалната порта преди #216 да е слят.

Слят преди #216, този клон би въвел харнеса, CI стъпката и конфигурациите за втори път → конфликт.

След като #216 се слее, клонът се rebase-ва върху main; vendor-натият commit (69d0a34) се свива (идентично съдържание), а нетното участие остава:

  • тестовете за покритие по всичките шест workspace-а;
  • vitest.shared.ts: изключване на **/fixtures/**, **/*.json, **/*.md, **/*.d.ts от инструментирането (данни/типове/докове, не изпълним код — иначе висят на постоянни 0% и v8 remap-ът чупи на markdown);
  • coverage-baseline.json: праговете вдигнати от началните на test: измерване на coverage и ratchet праг в CI (#93) #216 до текущите (виж таблицата).

Резултат

Workspace Редове Клонове Праг (редове/клонове)
apps/etl 100% 97.64% 100 / 97
apps/web 99.47% 96.21% 99.4 / 96
packages/config 100% 100% 100 / 100
packages/db 100% 98.47% 100 / 98.3
packages/ingest 100% 98.69% 100 / 98.7
packages/shared 98.87% 98.33% 98.8 / 98.3

Общо (информативно): 99.75% редове / 97.55% клонове. node scripts/check-coverage.mjs минава; собственият му self-test (check-coverage.test.mjs) също. CI е зелено (typecheck, lint, тестове).

Бележки

  • Нулева промяна в продуктов код — само тестове, конфигурация за покритие и базовата линия.
  • Реални поведенчески тестове, не cheater-и: маркер-базирани фалшиви D1 драйвери, които диспечират по SQL и хвърлят при неочаквана заявка; мокнати SDK/binding-и; проверки по байтове за BOM; end-to-end обхождане на курсори; adversarial входни данни.
  • Никакво отслабване на прага — базовата линия само се вдига, никога не се сваля.
  • Каква част от покачването идва от стесняване на знаменателя (по бележка на @ydimitrof): **/src/test/** е изключен като тестово скеле, и това мести само packages/ingest — 100%/98.69% с изключването срещу 98.97%/98.33% без него. Другите пет workspace-а са непроменени до последната цифра, и дори без изключването всичките шест остават над 95%. Изключеното е packages/ingest/src/test/d1-sqlite.ts — SQLite шим, който съществува само за да върти сюитите.
  • Умишлено непокрити остават само реално недостижими defensive клонове — ?? fallback-и върху regex групи, които винаги съвпадат; if(done) re-entry на stream (stream не дърпа след close()); split().pop() ?? (pop винаги връща стойност); homoglyph картата (всеки съвпаднал символ е мапнат); minContracts ?? DEFAULT, което оркестраторът вече нормализира; env-gated PROD пътища под vitest. Оставени са непокрити, а не заобиколени чрез отслабване на кода или v8 ignore прагми.

Свързани: #216 (харнес), #93 (епик за покритие).

ydimitrof and others added 30 commits July 16, 2026 18:28
Vendored from midt-bg#216 (feat/coverage-ratchet) to put the coverage machinery
in place ahead of midt-bg#217, while midt-bg#216 is pending merge upstream. Squashes
ydimitrof's two harness commits into one; original authorship preserved
via the commit author.

@vitest/coverage-v8 through a shared vitest preset, per-workspace coverage
configs with explicit include, committed coverage-baseline.json, the
scripts/check-coverage.mjs ratchet gate (+ node:test self-test), and the CI
wiring.

Ref midt-bg#93, midt-bg#216.
regionByName had zero tests (both lines uncovered). Adds its full branch
matrix (valid, whitespace-trim, null/undefined/empty/unknown, verbatim
round-trip of all 28 regions) plus edge inputs for categoryForDivision and
procedureGroup, and taxonomy-integrity invariants (unique CPV codes, single
procedure-type ownership, classified = competitive∪non-competitive,
28 unique NUTS3 regions). config: 92.85/72.22 -> 100/100 lines/branches.
… periodRange)

Adds eik/unp passthrough, one-sided and empty periodRange, date/monthYear/
longDate no-match + datetime-prefix + out-of-range-month fallbacks, count
sign/absence, pct/signedPct dp + non-finite, entityName non-collapsing paths,
cleanName unbalanced-quote drop, ЕТ/ET latin detection. shared branches
78.1 -> 98.3; residual is signedPct's provably-unreachable defensive return.

Caught: count(-0.4) emits '−0' (missing money()'s rounded-zero sign guard);
latent only (count takes non-negative integers), logged not patched.
New staging.test (0%->100%): scoped DELETE, chunked INSERT at CHUNK=100
boundaries (100/101/250 rows), null-fill of absent columns, table+column
routing per target. New refresh.test: SQL splitter (escaped '' in literals,
-- comments inside/outside literals, trailing statement), @refresh-batch
grouping, transient-table drop order, D1 orchestration. base/ocds additions:
toBool, Date.parse date fallback, annexes mapping, baseSqlLiteral numeric/
text/null branches, secured_inverse/variants coercions, party/lot/amendment
and catch-up-window branches. ingest lines 83.8->100, branches 78.9->95.1.
…dology, flows

identity slug fallbacks + undecodable name slug; keyset decodeCursor
malformed/oversized/bad-type-guard paths; home zero single-offer aggregate;
regions year + EU/national funding predicates; methodology absent-count
coalesce with positive total; flows sankey sort tiebreak on shared authority.
streamAuthority/Company/Contract sitemaps via a paginating fake D1: XML
escaping + C0 stripping, lastmod fallbacks (row date -> as_of -> none),
natural-person filtering, empty-chunk skip loop, CHUNK-boundary pagination,
contract page rowid windowing, and contractSitemapPages math. sitemaps
branch 34.9 -> 95.3; residual is the defensive post-close pull guard.
getCompany + getAuthority were entirely untested. Adds DTO assembly, share
math (won/spent denominators, zero-guards), avg-bids rounding, consortium
membership (list->participants, prose->note), hasEik, sector top6+tail rollup,
and getContract subcontractor (EUR/BGN/null/blank), framework call-off
detection, eurFromNative currency paths (EUR/BGN peg/FX/no-rate), deltaPct
suspect+zero-base guards, lot dedup/totals, and not-found. details branch
43.6 -> 88.5, lines 53 -> 100.
Adds the company-centre direction, null-param default (top authority) + its
empty fallback, includeCenterOptions=false, loadCenter sample-name fallback
(authority + company), hop-2 top-1-per-neighbour dedup, centre self-skip, and
edgeless-node weighting. network branch 50 -> 88, lines 86.7 -> 100.
… guards

search: empty/punctuation query -> empty shape, searchMoreHref unknown-kind
fallback. trend: YoY guarded against a zero prior year, coverage pct when
nothing is dated (no divide-by-zero).
Add coverage for the base-aggregation source (year/EU/single- vs multi-sector
primary_sector), the entity WHERE type/text filters, sort normalization, facet
label fallback and sort, page overflow, and the CSV stream body across the
CHUNK boundary. Branch 55%->96%, lines 100%.
Add coverage for sort normalization, the base-aggregation source (year/EU/
single- vs multi-sector), the kind/text entity WHERE, facet kind mapping and
sector sort, page overflow, missing total row, and the CSV body across the
CHUNK boundary. Branch 61%->95%, lines 100%.
Add coverage for the authority-detail wrappers (getAuthoritySingleOffer,
getAuthorityProcedureCompetition), getCompetitionSummary (both the qualifying
and null-topConcentration paths), the MAX_TOP cap, EU/national funding scope,
and a degenerate corpus exercising the zero-guard fallbacks. Branch 70%->95%.
Add coverage for buildFilters (every year/sector/procedure/value-bucket/EU/
bids/authority/bidder/text predicate), summary override, page overflow,
contractsSummary null row, listSingleOfferContracts modes, facet procedure
folding / sector sort / year ordering, and the streamed CSV body across the
CHUNK boundary. Branch 66%->96%, lines 100%.
Add degenerate-input coverage for getCompany (absent metadata/bids/suspect
rows, null primary sector and procedure value), getAuthority (spent-nothing
authority with an unknown CPV division, zeroed tail share, null bids/suspect),
and a getContract lot defaulting to the BGN peg. Branch 88%->98%, lines 100%.
Add coverage for an unresolvable company centre (null name → empty network),
the company-kind fallback when neither rollup nor sample carries a kind, the
empty-default path with includeCenterOptions off, and deduping a hop-1
neighbour that appears twice. Branch 88%->98%, lines 100%.
flows: EU/national/all funding scope + long-label truncation (branch 89->100).
search: nullish raw-query coalescing (branch ->97). trend: EU/national funding,
includeSectors=false, and an empty series with an absent coverage row
(branch 89->100).
index.ts: full RefreshWorkflow.run coverage (staging lifecycle, capped and
zero-ingest branches, derive-slice loop, finally-drop on success and error) and
the scheduled cron entrypoint, via mocked platform/ingest/eop seams. eop.ts:
bucket-key parse/classify, catch-up planning (uncapped/capped/default), bucket
listing status + redirect guard, OCDS/base staging, and the window walk.
Workspace 19%->98% branch, lines 100%.
Add coverage for singleSelectFilters (unknown sector/year flags, funding/top
defaults), buildSectorGroup (category grouping, summed vs absent counts,
uncategorised skip), sortHref, and the withParams/pageNav null-override, array,
empty-result, and page-default branches. filters.ts branch 47%->97%.
agent.ts: SDK-wiring coverage (model/base-URL resolution, tool-set assembly,
stream Response + onError) via mocked ai/@ai-sdk. report-schema.ts: flows block,
facts sub-line, unknown-handle, empty title, 0-row and null chart edges
(branch 78->94). tools.ts: run_sql AST-reject/error/meta-less paths, semantic
hits, eop_fetch, source_link (branch 57->95). Also exclude test fixtures and
type-only declarations from coverage (permanent 0% data files, not code).
render-format null-date; tool-results missing-cell + truncation flag; eop-fetch
null-date/non-array/invalid-JSON/thrown-fetch; rag embed mismatch + metadata
mapping + empty-vector guards; validateEmitShape callout/flows/timeseries and
the object/question/items/columns negatives.
Add coverage for the v0 freshness fallback, non-string/null q classification,
empty and empty-chunk multipart bodies, the abort-on-part-failure path, and
suffix (bytes=-N) / open-ended (bytes=A-) R2 range shapes (the fake now emits
R2-native range objects). csv-export lines 87->95, branch 76->90.
The v8 provider instruments every file matched by a workspace's include globs.
Markdown docs colocated in src (e.g. the assistant README) carry no coverable
statements, report a permanent 0%, and — being non-JS — make the reporter's
remap step throw a parse error. Exclude **/*.md alongside the existing JSON and
type-only exclusions so the ratchet total reflects executable code only.
Cover the remaining thin spots in the web workspace with real behavioural tests,
no code changes:

- cache.publicCache: default and explicit stale-while-revalidate windows
- eopSource: missing/malformed dates, DD.MM.YYYY key shape, OCDS cutoff boundary
- search.suggest: trimGroup cap + loader query/trim/headers, empty-q default
- app.ts hardening: nonce re-read path, OPTIONS short-circuit, no-Content-Type
- retry: non-Error rejection logging + default backoff past the table
- security: nonce-less headers omit the CSP outside production
- riskLogic: unknown bid count (null) and missing bidsRejected fallback
- ScrollToTop: rAF coalescing of a scroll burst

Web branch total 81.3% -> 95.47%, lines 89.3% -> 99.25%.
With the new tests in place, every workspace clears 95% on both lines and
branches. Regenerate the ratchet baseline from current coverage so the gate
now enforces the 95 floor going forward:

  etl      18.7/19.4  -> 100/98.5
  web      89.3/81.3  -> 99.2/95.4
  config   88.2/58.3  -> 100/100
  db       82/65.5    -> 100/97.3
  ingest   83.8/78.9  -> 100/95
  shared   94.8/78.1  -> 98.8/98.3
buildSectorGroup always returns categories, but the group type marks it
optional. vitest transpiles without type-checking so this passed locally;
tsc --noEmit under noUncheckedIndexedAccess (CI typecheck) rejected the
possibly-undefined access. Assert non-null at the three call sites.
Add branch-completion tests for releaseToContracts/Amendments/Lots on
releases with absent optional fields: missing tag/contracts keys, id-less and
identifier-less parties, a scheme-less CPV classification, an empty-string
value amount, a blank date, and an ocid/tender-id-less lot. ocds.ts branch
92.17% -> 98.26%, ingest workspace 95.06% -> 98.7%.

Residual uncovered branches are unreachable defensive code: the validDateOnly
regex reject (day is always pre-normalized to YYYY-MM-DD) and the
`rel.contracts ?? []` / `c.id ?? null` right-sides the length/id guards above
them make impossible.
- keyset: unsafe-direction guard, before-cursor with an ascending sort, and the
  empty before-page (both cursors null)
- flows: two-authority sankey so the authority-column sort comparator runs
- regions: empty dataset → the total==0 coverage-pct guard (no divide-by-zero)
- companies: base aggregation from a non-sector filter (no CPV predicate)
- contracts: the „Неизвестна" year bucket sinking below real years regardless
  of input order
- authorities/companies/contracts: backward pagination — page forward for a
  cursor, back for a before-cursor, then feed it back so keyset's reverse path
  runs

db branch 97.35% -> 98.39%. Residual gaps are unreachable defensive code:
CSV/sitemap `if(done)` re-entry (a stream never pulls after close), the
minContracts `?? DEFAULT` the orchestrator already normalises, split().pop()
`?? ` fallbacks (pop is always defined), the homoglyph map (every regex-matched
char is mapped), and cross-namespace network self-edges.
- assertReadOnlySelect: empty/comment-only query, a forbidden keyword hidden
  in a single CTE-prefixed statement (cheap keyword layer, not just the AST
  guard), and the sqlite_master/sqlite_schema catalog-table rejection
- run_sql: a driver returning no results array (the results ?? [] fallback)
- eop-fetch: a non-Error thrown value → the generic fetch-error label

web branch 95.47% -> 96.0%. Residual gaps are deep AST-shape defenses
(sql-ast-guard), schema-validation guards (report-schema), and unreachable
code: regex capture-group ?? fallbacks (csp, always matched), PROD-gated
redirect/OPTIONS paths under vitest, and the module-init Date.now tag.
Coverage rose across web/db/ingest with the new branch tests; raise the
ratchet floors to match (never down):

  web      99.2/95.4 -> 99.4/96
  db       100/97.3  -> 100/98.3
  ingest   100/95    -> 100/98.7

etl (100/98.5), config (100/100) and shared (98.8/98.3) unchanged. Monorepo
total 99.73% lines / 97.51% branches.
…g#254 review)

The comment said a whitespace-only value 'trims to ""' and grouped it with the nullish
inputs, which reads as if it hits the `if (!procedureType)` guard. It does not: '   ' is
truthy, passes the guard, and reaches the map lookup, where .get('') misses and the ??
fallback supplies PROCEDURE_UNKNOWN. Spelling out both paths so the map-miss branch is
visibly exercised here as well as by the unrecognised-type case above.
…table midt-bg#312)

Fifth sync. Upstream midt-bg#312 turned /conflicts from a card list into a one-row-per-person
table and renamed ConflictCards → ConflictDetail, which the person/company pages now
render eagerly. One conflict, in conflicts.render.test.tsx.

Resolution: took upstream's file whole rather than union-merging. Our tests there
targeted the card LIST — expand-to-drill-down, per-card contract lists — and that route
no longer renders cards at all, so keeping them would have asserted against a UI that
does not exist. Upstream's 16 table tests replace them on their own terms.

That resolution cost real coverage, because the component SURVIVED the route change
while its tests did not: apps/web branches fell to 94.71% (floor 96), with 18 uncovered
branches concentrated in ConflictDetail.tsx. Restored with tests against the component
directly rather than by moving the floor:

- apps/web/app/components/ConflictDetail.test.tsx (new, 10 tests) — the thinner row
  shapes the feed really produces: a 'confirmed' seal that cites no register act (no
  bare „№"), a link with no source declaration, a declaration with no usable period, a
  sub-threshold authority share („под 0,1%", track but no fill), a share with no
  denominator („—", no track at all — „0%" would be a different and false claim), a
  winner whose amounts are all NULL, a timeline with marks but no band, unnumbered
  contracts (index-keyed, two rows not one), and an unresolved awarding body („—").
  ConflictDetail branches 76/94 → 91/94.

- conflicts.test.ts — declaredStakeNoun's mixed-page branch. A page carrying both a
  self stake and a family stake must fall back to the neutral wording; either
  single-sided noun would be false about half its cards, and asserting „собствен дял"
  over a family card is a false claim about a named individual.

Deliberately left uncovered: the officialSlug tie-break in groupByPerson's comparator.
Its own comment documents it as unreachable belt-and-braces (isStrongerLink already
breaks every tie on the globally unique link_key), kept for totality.

Also repaired one test the merge broke: related-persons.test.ts registered contract rows
under a bare link_key, but upstream's improved fake namespaces contract reads under
`contracts:<key>` so a scope query can never be mistaken for a contract read. Verified
still discriminating — leaking `r.authority` instead of `?? ''` fails it.

Coverage after: etl 100/97.64, web 99.42/95.96, config 100/100, db 100/98.12,
ingest 100/98.69, shared 98.87/98.33. Gate green, no floor moved.
@lyubomir-bozhinov

Copy link
Copy Markdown
Collaborator Author

@todorkolev @ydimitrof PR-ът е готов и стои само на натискането на бутона.

Състояние на 257b097: CI зелено (4/4), mergeable, 0 коммита зад main. Нулева промяна в продуктов код — само тестове, конфигурация за покритие и базовата линия.

Пети sync — #312 е поет

Таблицата по лице смени /conflicts от карти на един ред на човек и преименува ConflictCards → ConflictDetail. Един конфликт, в conflicts.render.test.tsx — взех вашия файл изцяло, а не union: нашите тестове там караха картовия СПИСЪК (разгъване, списък с договори в картата), а този маршрут вече не рендерира карти изобщо.

Това обаче костваше реално покритие, защото компонентът оцеля, а тестовете му — не: apps/web клони паднаха на 94.71% при праг 96, с 18 непокрити клона в ConflictDetail.tsx. Върнато с тестове, не с местене на прага:

  • ConflictDetail.test.tsx (нов, 10 теста) срещу самия компонент — формите, които фийдът реално дава: печат confirmed без акт в регистъра (без гол „№"), връзка без декларация-източник, декларация без използваем период, подпрагов дял („под 0,1%" — писта без пълнеж), дял без знаменател („—" и никаква писта, защото „0%" е друго и невярно твърдение), изпълнител с NULL суми, времева ос с маркери но без band, договори без номер, неразпознат възложител. ConflictDetail 76/94 → 91/94.
  • declaredStakeNoun — смесената страница. Страница с едновременно собствен и семеен дял трябва да падне на неутралната формулировка; „собствен дял" над семейна карта е невярно твърдение за поименно лице.

Умишлено непокрит остава tie-break-ът по officialSlug в компаратора на groupByPerson — собственият му коментар го описва като недостижим (isStrongerLink вече чупи всяко равенство по глобално уникалния link_key), държан само за тоталност. Не го покривам изкуствено.

Покрай това поправих един тест, който сливането счупи: related-persons.test.ts регистрираше договорни редове под гол link_key, а подобреният fake ги namespace-ва под contracts:<key>. Проверено, че още различава — изтичане на r.authority вместо ?? '' го чупи.

Покритие след сливането: etl 100/97.64, web 99.42/95.96, config 100/100, db 100/98.12, ingest 100/98.69, shared 98.87/98.33. Нито един праг не е местен.

@ydimitrof — отворените нишки

Минах през всичките още веднъж срещу текущия HEAD:

  • Адресирани: инвариантът за data_freshness в eop.test.ts; референцията в search.suggest.test.tsx (вече сочи feat(db,web): app-layer read-only D1 guard — getDb chokepoint над env.DB (#199) #225); fake-ът в trend.test.ts; retry.test.ts вече ползва vi.useFakeTimers().
  • Не се потвърждават две: ' ' е truthy, тъй че минава през if (!procedureType) и стига до map lookup-а — map-miss клонът се изпълнява, и е покрит отделно и от случая с неразпозната процедура (изострих коментара, който подведе, в 940296f); а лимитът в listSingleOfferContracts се асертира на място — LIMIT ? плюс binds[0] да е [5].
  • Валидно и пренесено: bind-аргументите са слабо асертирани в повечето fake-ове. Това е test: споделен помощник за фалшив D1 вместо ~130 локални двойника #325, който пое — там му е мястото, не тук.
  • Отклонено: праг под 100 „за запас". tolerance: 0.5 вече е този запас, а праговете тук вървят само нагоре.

Молба

@todorkolev, нямам write достъп (triage), тъй че не мога да го слея сам. PR-ът е на пети sync и всеки път поема нова upstream работа — колкото по-дълго стои, толкова повече конфликти събира. Ще го погледнеш ли?

ydimitrof added a commit to ydimitrof/sigma that referenced this pull request Aug 26, 2026
midt-bg#325: 24 test files hold 36 `as D1Database` casts, one hand-rolled double each.
Every one dispatches on `sql.includes('…')` and falls through to `{ results: [] }`
when no marker matches, so renaming a CTE or reordering a JOIN leaves the test
green against emptiness — asserting nothing. Only details.test.ts throws today.

This is the acceptance test for that work, written before the work: outside an
explicit allowlist, no file under apps/ or packages/ may type a value as a
D1Database. It is red now (24 files, 36 casts) and goes green when the last
double moves to the shared helper.

The allowlist is by name, never a directory glob — the argument the midt-bg#254 review
already made about the coverage exclusion list. A glob lets a new double leave
the gate by where it sits; a named entry means someone had to add it, which is
reviewable. A stale entry is an error rather than a no-op, so a renamed double
cannot leave the gate widened by a line nobody reads again. That fail-closed
branch is what fires right now, since the helper does not exist yet.

Matching is over blanked source — comments, strings and regex literals removed,
byte positions kept — so a comment describing the old design is not a finding.
`as unknown as D1Database` is matched before `as D1Database` because the short
spelling is a suffix of the long one and a naive pattern counts one cast twice.
`satisfies` is covered too: it is the only other operator that types a literal.

Self-test is mutation-checked — dropping the `as unknown as` alternative, the
`satisfies` alternative, the comment blanking, the trailing word boundary, or
the stale-entry check each kills exactly one named test, and no others.

scripts-test.yml needs no edit: its lane already globs scripts/*.test.mjs.
ydimitrof added a commit to ydimitrof/sigma that referenced this pull request Aug 26, 2026
The helper midt-bg#325 asks for, as its own private workspace. `@sigma/db` exports only
`.`, and neither apps/etl nor packages/ingest depends on it, so putting the
double under db/src/test/ would have meant a subpath export plus two new
workspace deps. A separate package also sits outside all six measured
workspaces, so it cannot enter their coverage denominators by construction —
stronger than the by-name vitest.shared.ts exclusion the issue proposes, and it
leaves that file (which midt-bg#254 rewrites) untouched.

A route is a marker set and a response; every marker must appear in the SQL, and
the first matching route wins so a specific route can precede a general one.
Unmatched throws, naming the offending statement and every registered marker.
`{ onUnmatched: 'empty' }` buys emptiness back, at the call site, in writing.

Three entry points, one core: fakeD1 for query tests, recordingD1 for the tests
of a *wrapper* over D1 (readonlyD1) that must accept arbitrary SQL and assert on
a call log, throwingD1 for the error paths.

Two design notes worth keeping:

  - `first` is typed `object | null | (call) => object | null`, not `unknown`.
    A top type absorbs the union and the callback form silently loses its
    parameter type — tsc caught it. A D1 row is an object or nothing anyway.
  - No pagination feature. Keyset slicing is already `all: (call) =>
    rows.filter(r => r.id > call.binds.at(-2))`, which is what the doubles in
    companies.test.ts do by hand today.

Tests written before the code, behaviour by behaviour, and mutation-checked:
never throwing on an unmatched all() or first(), matching a route that answers a
different method, `some` for `every` over the markers, last-match instead of
first, no truncation, dropping the marker list from the message, discarding
bind() arguments, not recording prepare() or batch(), and ignoring throwingD1's
supplied error — eleven mutations, each killing a specific named test.

The `?? []` fallback in all() went away rather than getting a test: the route
lookup already guarantees the response is defined, so it was unreachable.
Returning the response instead of the route also keeps `first: null` — a route
meaning "no such row" — distinct from no route at all.

Seventh key in coverage-baseline.json: check-coverage's findTestWorkspaces fails
closed on a workspace that has a test script without a baseline entry, and this
one should be measured. 100% lines, 100% branches. The six existing workspaces
are untouched — they were already reading above their baselines before this
branch, which is pre-existing drift and not for a test refactor to ratchet.
ydimitrof added a commit to ydimitrof/sigma that referenced this pull request Aug 26, 2026
d1FromSqlite lived in packages/ingest/src/test/, and packages/db had two
byte-identical re-implementations of it (contracts-filter-sql, value-base-sql,
differing only in a local variable name) while apps/etl reached the original
through ../../../packages/ingest/src/test/d1-sqlite — a relative path across a
workspace boundary, which is what a missing shared home looks like.

It moves to @sigma/test-support beside the fake. The two are different tools and
stay different: this one runs the real SQL against a real node:sqlite database
where SQL semantics are what is under test; fakeD1 is for the TypeScript logic
around a query. Now they at least live in the same place, and the gate's
allowlist names one package instead of two.

Slightly wider than midt-bg#325 asked — the issue scopes itself to the fake doubles and
puts real-SQLite tests out of scope. It is here because "one cast everywhere" is
one of its own done-when boxes, and two of the four remaining casts were these
copies. Reviewer's call; it lifts out cleanly.

Side effect worth noting: d1-sqlite.ts leaves packages/ingest's coverage
denominator by leaving the workspace, which is the outcome midt-bg#254 wanted from a
by-name exclusion, reached by construction instead.

db 487, ingest 84, etl 20 — all pass.
todorkolev added a commit that referenced this pull request Aug 26, 2026
* test(ci): gate the fake-D1 doubles, and fail on an unmatched query

#325: 24 test files hold 36 `as D1Database` casts, one hand-rolled double each.
Every one dispatches on `sql.includes('…')` and falls through to `{ results: [] }`
when no marker matches, so renaming a CTE or reordering a JOIN leaves the test
green against emptiness — asserting nothing. Only details.test.ts throws today.

This is the acceptance test for that work, written before the work: outside an
explicit allowlist, no file under apps/ or packages/ may type a value as a
D1Database. It is red now (24 files, 36 casts) and goes green when the last
double moves to the shared helper.

The allowlist is by name, never a directory glob — the argument the #254 review
already made about the coverage exclusion list. A glob lets a new double leave
the gate by where it sits; a named entry means someone had to add it, which is
reviewable. A stale entry is an error rather than a no-op, so a renamed double
cannot leave the gate widened by a line nobody reads again. That fail-closed
branch is what fires right now, since the helper does not exist yet.

Matching is over blanked source — comments, strings and regex literals removed,
byte positions kept — so a comment describing the old design is not a finding.
`as unknown as D1Database` is matched before `as D1Database` because the short
spelling is a suffix of the long one and a naive pattern counts one cast twice.
`satisfies` is covered too: it is the only other operator that types a literal.

Self-test is mutation-checked — dropping the `as unknown as` alternative, the
`satisfies` alternative, the comment blanking, the trailing word boundary, or
the stale-entry check each kills exactly one named test, and no others.

scripts-test.yml needs no edit: its lane already globs scripts/*.test.mjs.

* test(test-support): a shared D1 double that throws on an unmatched query

The helper #325 asks for, as its own private workspace. `@sigma/db` exports only
`.`, and neither apps/etl nor packages/ingest depends on it, so putting the
double under db/src/test/ would have meant a subpath export plus two new
workspace deps. A separate package also sits outside all six measured
workspaces, so it cannot enter their coverage denominators by construction —
stronger than the by-name vitest.shared.ts exclusion the issue proposes, and it
leaves that file (which #254 rewrites) untouched.

A route is a marker set and a response; every marker must appear in the SQL, and
the first matching route wins so a specific route can precede a general one.
Unmatched throws, naming the offending statement and every registered marker.
`{ onUnmatched: 'empty' }` buys emptiness back, at the call site, in writing.

Three entry points, one core: fakeD1 for query tests, recordingD1 for the tests
of a *wrapper* over D1 (readonlyD1) that must accept arbitrary SQL and assert on
a call log, throwingD1 for the error paths.

Two design notes worth keeping:

  - `first` is typed `object | null | (call) => object | null`, not `unknown`.
    A top type absorbs the union and the callback form silently loses its
    parameter type — tsc caught it. A D1 row is an object or nothing anyway.
  - No pagination feature. Keyset slicing is already `all: (call) =>
    rows.filter(r => r.id > call.binds.at(-2))`, which is what the doubles in
    companies.test.ts do by hand today.

Tests written before the code, behaviour by behaviour, and mutation-checked:
never throwing on an unmatched all() or first(), matching a route that answers a
different method, `some` for `every` over the markers, last-match instead of
first, no truncation, dropping the marker list from the message, discarding
bind() arguments, not recording prepare() or batch(), and ignoring throwingD1's
supplied error — eleven mutations, each killing a specific named test.

The `?? []` fallback in all() went away rather than getting a test: the route
lookup already guarantees the response is defined, so it was unreachable.
Returning the response instead of the route also keeps `first: null` — a route
meaning "no such row" — distinct from no route at all.

Seventh key in coverage-baseline.json: check-coverage's findTestWorkspaces fails
closed on a workspace that has a test script without a baseline entry, and this
one should be measured. 100% lines, 100% branches. The six existing workspaces
are untouched — they were already reading above their baselines before this
branch, which is pre-existing drift and not for a test refactor to ratchet.

* test(test-support): keep `sql` live, and fail the throwing double at execution

Two defects the first migration batch walked straight into.

`sql` was a getter over `calls`, so `const { db, sql } = fake()` — the natural
way to use it, and what flows.test.ts and authorities.test.ts already wrote
against their hand-rolled spies — captured an empty snapshot that never filled
in. Every later assertion then read nothing and passed for the wrong reason,
which is the exact failure this helper exists to remove. It is a live array kept
in step with `calls` now, and a test pins the destructured form.

throwingD1 threw from prepare(). D1's prepare() is lazy and never touches the
database: a missing table surfaces on all()/first()/run(). A double that failed
earlier would let a test claim it covers an error path it never reaches — and
related-persons.test.ts, whose whole point is that an un-migrated environment
degrades instead of 500ing, hand-rolled a double that threw at execution for
exactly that reason. It now rejects from the three execution methods and records
the statement that failed, so the offending SQL stays inspectable.

* test(db): route the query doubles through the shared fake

Sixteen files in packages/db/src/queries, each of which built its own D1 double
that dispatched on `sql.includes('…')` and fell through to no rows. Fixtures and
assertions are unchanged — only the double moves.

Measured before touching anything, by breaking each marker in the production SQL
and running the test: ELEVEN marker paths across nine files stayed green against
an emptied result. authorities (FROM authority_totals), companies (ORDER BY
bidder_id), competition and trend and flows (FROM sector_totals), contracts
(facet_counts), home (bids_received = 1, JOIN), network (FROM company_totals,
FROM authority_totals WHERE authority_id), search (sqlite_master). Every one of
them now rejects with the marker set it was looking for.

Three things the migration turned up that were not in the issue:

  - regions.test.ts served *region* rows to sectorOptions, which asks a
    completely different table. It reached the same answer only because
    sectorOptions reads r.division, the region fixture has no such field, and
    the filter dropped every row. The route says `all: []` now, and says why.
  - companies.test.ts registered two facet routes for queries no test in it ever
    issues — getCompanyFacets is not exercised there. Dropped rather than kept
    as decoration.
  - companies' CSV stream and list query both read company_totals, so breaking
    the stream's ORDER BY quietly fell through to the list route and returned an
    unpaginated page. They are separated by their own markers now (ORDER BY
    bidder_id vs AS sort_value), and breaking either one throws.

Two markers still survive being broken — authorities' and companies' `FROM
<rollup>`. That is the harness, not the tests: `FROM ${src.from}` is composed at
runtime, so the literal never appears in the source to be mutated. Mutating the
`from:` value itself is caught by both.

Route matching is still substring-based, so a query can fall from a specific
route to a more general one in the same set. What is gone is the *default*
fall-through to emptiness — an unrouted query throws.

packages/db: 487 tests pass; coverage unmoved.

* test(test-support): one real-SQLite D1 facade instead of four copies

d1FromSqlite lived in packages/ingest/src/test/, and packages/db had two
byte-identical re-implementations of it (contracts-filter-sql, value-base-sql,
differing only in a local variable name) while apps/etl reached the original
through ../../../packages/ingest/src/test/d1-sqlite — a relative path across a
workspace boundary, which is what a missing shared home looks like.

It moves to @sigma/test-support beside the fake. The two are different tools and
stay different: this one runs the real SQL against a real node:sqlite database
where SQL semantics are what is under test; fakeD1 is for the TypeScript logic
around a query. Now they at least live in the same place, and the gate's
allowlist names one package instead of two.

Slightly wider than #325 asked — the issue scopes itself to the fake doubles and
puts real-SQLite tests out of scope. It is here because "one cast everywhere" is
one of its own done-when boxes, and two of the four remaining casts were these
copies. Reviewer's call; it lifts out cleanly.

Side effect worth noting: d1-sqlite.ts leaves packages/ingest's coverage
denominator by leaving the workspace, which is the outcome #254 wanted from a
by-name exclusion, reached by construction instead.

db 487, ingest 84, etl 20 — all pass.

* test(db): recording doubles for the two readonly wrapper suites

readonly-d1 and readonly-corpus test a *wrapper* over D1, not a query: what
matters is which statements reach the handle underneath, not what comes back.
Marker dispatch is the wrong shape for that, so both use recordingD1 — answers
anything, records everything — with `when: []`, a route that constrains nothing.

Two things came out of it, both in the helper:

  - `when: []` matching every query was already true (every() over no markers),
    but undocumented and unpinned. Now both.
  - readonly-d1's hand-rolled log tagged its entries `prepare:` / `exec:`, and
    flattening that into plain SQL would have cost the test its point: a wrapper
    that sent an exec down the prepare path emits identical text, and the
    assertion could no longer tell. FakeD1Call carries `via` now, and the
    corpus's zero-proxy row survives as the response to a constraint-free route.

readonly-corpus also dropped a `raw()` no production path calls.

packages/db: 487 tests pass.

* test(etl): route the ETL doubles through the shared fake

Three doubles. The integrity gate's fake dispatched on eleven markers and fell
through to no rows; it now names all eleven as routes and rejects anything else.
Its local builder was called `fakeD1`, which is the shared helper's name, so it
becomes `servedD1` — which is what it models anyway: a served D1 after
precompute, not any old one.

eop.test.ts also passed `{} as D1Database` twice, for paths that fail before
they reach the database. `fakeD1([])` states that instead of implying it: a
route-less double rejects any query, so if one of those paths ever did reach D1
the test would say so rather than throwing an incidental TypeError on an empty
object.

The freshness double's guard survives as a route that throws its own message —
"raw staging should not be read for planning" is a claim worth keeping in the
test, rather than degrading to the generic no-route error.

apps/etl: 20 tests pass.

* test(test-support): per-route meta, and cover the SQLite facade in its new home

apps/web's assistant tests need meta.rows_read and meta.total_attempts: they
drive the rows-read budget that keeps a retried full scan from under-billing the
Denial-of-Wallet limit (#122, review #80). Flattening that to a fixed empty meta
would have quietly removed what those two tests assert, so a route can declare
its own meta. Default stays `{}`.

Moving d1-sqlite.ts here left it with no tests of its own — its callers live in
db, ingest and etl, and none of them count toward this workspace. The ratchet
caught it at 81% and it is covered directly now, including the case nothing
tested anywhere before: batch() rolls back when one statement fails. A
half-applied batch would leave a fixture in a state no production path can
reach, and whoever met it would be debugging a ghost.

While covering it, throwingD1's bind() read `calls.at(-1)` — so binding statement
A after preparing B recorded the arguments against B. Same statement-independence
bug fakeD1 already had a test against; it captures its own record now, and so
does the test.

100% lines, 100% branches, 44 tests.

* test(web): route the last two doubles through the shared fake

assistant/tools built a double whose only real job was carrying meta; it now
declares that meta on a route. csv-export asserted the expected SQL *inside* its
fake — that assertion becomes the route's own marker, so a query that no longer
matches rejects and names both the statement and what was expected, instead of
failing an inline expect from inside a stub.

With these two the gate from the first commit goes green: 279 files scanned, no
D1Database cast outside @sigma/test-support. It opened at 36 casts in 24 files.

apps/web: 493 tests pass.

* fix(test-support): route exec() and batch() through the same contract

batch() recorded each statement and returned a synthetic success without ever
consulting the routes, so a batch of unregistered SQL passed against nothing —
the silent green this helper exists to kill, on the one entry point the write
paths use exclusively (staging, refresh, fx never call prepare().run()).
exec() had the identical hole one method up.

Both now look the statement up. They ask only whether it is registered at all,
not for a particular response shape the way all()/first()/run() do, and throw
naming the SQL and every marker when it is not.

Also: run() and batch() carry the `results` key a real D1Result always has —
the cast to D1Database was hiding its absence; the header no longer points at
the facade's pre-move path; and the second batch() record is documented as a
log of entry points rather than a double count.

* fix(test-support): carry the full D1Result shape in the SQLite facade

all() returned no `meta`, run() neither `meta` nor `results`, batch() no
`results`. The cast to D1Database hid every one of them: the first caller to
read one would get `undefined` from the facade where real D1 hands back `[]`
or `{}`. One D1Shape type spells out all three keys.

* test(web): keep the exact-SQL check csv-export had before the migration

The hand-rolled double asserted `expect(sql).toBe(...)` on the whole statement.
Migrating turned that string into a `when` marker, and markers match by
substring — so the one place the refactor loosened a check rather than
tightening it. Measured: wrapping the production statement leaves all 34 tests
green. The equality moves inside the route, where the callback sees `call.sql`.

* test(ci): pin the gate's scan roots and catch an aliased D1Database

Two ways past the gate, both reproduced. `type DBAlias = D1Database` and then
`as unknown as DBAlias` leaves no D1Database token for the pattern to find; a
renamed type import does the same. A second pass treats giving the type another
name outside the allowlist as the offence, while leaving ordinary annotations
(`db: D1Database`, a field on an Env type) alone.

And SCAN_ROOTS was module-private, so deleting 'apps' from it left the self-test
12/12 green while web and etl dropped out of enforcement. Exported and pinned: a
pattern applied to half the repo is a gate that passes while enforcing nothing.

* fix(test-support): route a batched statement by what it does, not its markers

Markers alone are blind to the method, and it is measurable: a route declaring
only `all:` answered a batched `DELETE FROM staging` with its rows, because
`FROM staging` is a substring of the write. The same SQL through prepare().run()
threw. Narrower than an unrouted batch, but the same silent pass.

A batched write now needs a `run:` route and a batched SELECT an `all:` one;
neither settles for the other, and a SELECT no longer fires a write effect it
happens to match. A write still serves rows when it has them, for RETURNING.
exec() asks for `run:` too — it hands back no rows, so nothing else means
anything to it. That retires `registered()`: every entry point is method-aware.

Reading is decided by the leading keyword, so a `WITH … INSERT` reads as a
SELECT here. That costs a false rejection, never a false pass.

Also: run() carries the route's meta, which batch() already resolved for the
same statement, and batch() documents that it is not transactional — real D1
and the d1-sqlite.ts facade roll back, this does not.

* test(ci): widen the alias rule to where an alias actually goes

The pattern closed three spellings while the comment promised the class. Four
more walked past it: `D1Database & {}`, `Pick<D1Database, …>`, a namespaced
`import('…').D1Database`, and a heritage list naming it off the first position.

The rule is now positional — the mention must sit right of `=`, or inside an
intersection, union, type argument or namespace, never where a parameter or a
field goes. `type Env = { DB: D1Database }` and the conditional type in
readonly-corpus.test.ts stay clean, both pinned.

`implements` is deliberately out: ReadonlyD1 implements D1Database in
production, and TypeScript forces a complete implementation there, so it is no
shortcut to a stub. The comment now says best-effort and means it.

---------

Co-authored-by: Todor Kolev <tkolev@obecto.com>
@todorkolev

Copy link
Copy Markdown
Collaborator

Основата под този PR се размести, откакто беше отворен - две неща вече са на main и променят какво остава да се направи тук.

#216 е слято. Ти vendor-ваше харнеса му едно към едно (scripts/check-coverage.mjs, vitest.shared.ts, coverage-baseline.json, per-workspace конфизите). Сега те съществуват независимо на main, тоест байтовите копия тук ще дадат конфликт или дублиране при изравняване. Оправянето е да махнеш vendor-ната част и да стъпиш на вече слятата.

#331 е слято току-що. Замени ~29 ръчни фалшиви D1 двойника с общия @sigma/test-support (fakeD1, recordingD1, throwingD1), в който непозната заявка хвърля. Тук се застъпваме по 20 файла, повечето фалшиви D1 тестове - и точно това затваря голяма част от нишките по-долу. Затова редът вече е: ребейз върху main (с #331), после дописване.

Актуалните нишки от прегледа на @ydimitrof, групирани:

Вероятно се решават от само себе си при мигриране към @sigma/test-support (общият двойник хвърля при непозната заявка и записва bind-параметрите, което е точно каквото нишките искат):

  • contracts.test.ts:87 - bind() изхвърля аргументите, тоест не се проверяват стойностите/редът на bind. recordingD1 ги пази.
  • contracts.test.ts:398 - фалшивият обект връща един ред независимо от LIMIT, значи toHaveLength(1) е тривиално. Двойник, който хвърля при непознат SQL, не позволява такова мълчаливо съвпадение.
  • eop.test.ts:99 - fakeDbFromFreshness игнорира SQL-а и винаги връща freshness реда, губейки предишната защита „raw staging не се чете за планиране". Маршрут-базираният двойник я връща.
  • trend.test.ts:186 - фалшивият all() връща series за всяка заявка, вкл. sector_totals. По-строг двойник го прави явно.

Остават за твоя преценка след ребейза:

  • config/src/index.test.ts:143 - коментарът си противоречи: ' '.trim() е '', тоест falsy, не truthy; трябва да отрази реалния falsy-пазач.
  • contracts.test.ts:137 - асерциите са плътно свързани с точния SQL низ; поведенчески еквивалентен рефактор ще ги счупи без реален дефект.
  • retry.test.ts:51 - vi.spyOn(setTimeout) без mockImplementation реално чака ~350ms; vi.useFakeTimers() маха забавянето.
  • coverage-baseline.json:5 - прагът за lines е точно 100 при tolerance: 0.5, тоест практически нулев буфер: следващ PR с един непокрит ред в този workspace чупи CI, включително несвързани PR-и. Струва си малък марж.
  • search.suggest.test.tsx:4 - недовършена препратка „upstream #…" в коментар.

Само тестове, критичният път за свързаните лица не минава оттук - тоест няма спешност. Като изравниш върху main и минеш през горното, кажи и ще погледна пак. Няма да мърдам клона ти без да питам.

Adopts midt-bg#331's shared fake-D1 double and its check:fake-d1 gate across every
suite this branch adds, and midt-bg#334's CACBG corpus work.

- migrate all 71 hand-rolled D1 doubles (18 files) to @sigma/test-support:
  fakeD1 for query tests, recordingD1 for the two ingest wrappers whose SQL is
  generated rather than routed, throwingD1 for the run_sql error path. An
  unmatched query now throws instead of answering with no rows, which surfaced
  two silent gaps: getCompany/getAuthority never declared the listContracts
  panel reads, and eop/etl passed `{}` as a binding that could never fail.
- keep the doubles that must record batch GROUPING (ingest staging/refresh) by
  wrapping batch() over the shared double rather than re-rolling one.
- coverage-baseline.json: union of this branch's raised floors and upstream's
  new packages/test-support entry; apps/etl branches ratcheted 97 -> 97.6.
- vitest.shared.ts: drop the now-stale src/test/d1-sqlite.ts exclusion (midt-bg#331
  moved that shim into packages/test-support, which carries its own entry).

All seven workspaces stay at or above their floors; check-fake-d1, check-docs
and check-coverage are green.
Line-length reflow only — no assertion or fixture changes. The three files
whose fakeD1 call sites pushed a line past printWidth after the midt-bg#331 migration.
@lyubomir-bozhinov

Copy link
Copy Markdown
Collaborator Author

@todorkolev Изравнено върху main (с #331 и #334) в c9e842b. CI е 4/4 зелено на fd2bc1b, MERGEABLE, 0 коммита назад.

#331 — мигрирано изцяло

Всичките 71 ръчни двойника в 18 файла минаха на @sigma/test-support. check-fake-d1 е зелено: 292 file(s) scanned, no D1Database cast outside the 2 allowlisted.

Разпределението не е механично — трите помощника решават различни задачи:

  • fakeD1 за заявковите тестове (db/queries/*, details, contracts, authorities, companies, competition, search, sitemaps, network, trend, regions, home).
  • recordingD1 за двата ingest wrapper-а (staging.ts, refresh.ts). Там SQL-ът се генерира, а не се маршрутизира — маркерна диспечеризация няма по какво да хване. Това, което на тези тестове им трябва отгоре, е групирането по batch (кои statement-и излизат заедно), а плоският лог не го пази — затова batch() е обвит над двойника и реже лога при всяко извикване. Обвиване, не пре-имплементиране: повърхността остава на @sigma/test-support.
  • throwingD1 за грешковия път на run_sql.

Строгостта хвана два реални пропуска, не само стилови:

  1. getCompany / getAuthority никога не декларираха четенията на панела „последни договори" (listContracts). Старият двойник ги обслужваше с else return { results: [] } — тоест панелът се е тествал срещу празнота. Сега са изрични маршрути.
  2. eop.test.ts и index.control-flow.test.ts подаваха {} as D1Database — връзка, която не може да се провали. Сега е fakeD1([]) без нито един маршрут: всяко докосване на базата хвърля. Същото за agent.test.ts, tools.test.ts, search.suggest.test.tsx.

По нишките, които ти групира като „вероятно се решават от само себе си" — решават се, но с проверка, не по презумпция:

  • contracts.test.ts:87 — listSingleOfferContracts вече твърди срещу calls[0].binds ([5] при явен лимит, [10] по подразбиране), а не само срещу SQL текста.
  • contracts.test.ts:398 — препълването на страницата вече е 2 реда при pageSize: 1, тоест toHaveLength(1) е реално отрязване, не тавтология.
  • eop.test.ts:99 — fakeDbFromFreshness има само маршрут data_freshness; четене от raw staging при планиране хвърля.
  • trend.test.ts:186 — sector_totals е отделен маршрут с празен отговор.

#216 — две уточнения

Vendor-ната част я няма. Проверено байт по байт срещу main:

Файл Срещу main
scripts/check-coverage.mjs идентичен
packages/{db,ingest,shared,config}/vitest.config.ts идентични
apps/{web,etl}/vitest.config.ts идентични

Тоест по тези седем файла няма какво да се маха — дифът им срещу main е нула. Различават се точно два, и то защото те са товарът на този PR, не копие:

  • coverage-baseline.json — само вдигнати прагове (etl 74/58.2 → 100/97.6; web 91/82.4 → 99.4/96; config 92.8/72.2 → 100/100; db 94.5/79.3 → 100/98.3; ingest 86.3/80.4 → 100/98.7; shared 95.5/80.8 → 98.8/98.3), плюс новия запис packages/test-support от test: споделен фалшив D1, който хвърля при непозната заявка #331, взет както е.
  • vitest.shared.ts — файлът на main плюс 20 реда списък с изключвания. Чист прираст, +20 -0.

Махнах и остарялото '**/src/test/d1-sqlite.ts' — #331 премести шима в packages/test-support, който има собствен запис в базовата линия.

За прага lines: 100. Буферът вече съществува и е точно tolerance. scripts/check-coverage.mjs:83 сравнява pct < base - tolerance — при 100 и tolerance: 0.5 заданието пада едва под 99.5%, не при първия непокрит ред. Свалянето на праг е единственото, което няма да направя; ако буферът трябва да е по-голям, редът за това е tolerance, не базовата линия.

Три от петте „остават за преценка" бяха вече свършени

Не е спор — просто нишките на @ydimitrof не се резолват, тъй че списъкът се чете от състояние отпреди фиксовете:

  • config/src/index.test.ts — коментарът е пренаписан в 940296f, и има уточнение по същество: procedureGroup не trim-ва преди пазача. Източникът е if (!procedureType) return …; return MAP.get(procedureType.trim()) ?? …. Значи ' ' е truthy, минава пазача и стига до .get(''), което не уцелва → fallback-ът е ?? PROCEDURE_UNKNOWN. undefined/null са другият път — хващат се от пазача. Коментарът именува и двата.
  • retry.test.ts — вече е vi.useFakeTimers() (apps/web/app/lib/retry.test.ts:49). Няма реално чакане.
  • search.suggest.test.tsx — препратката е допълнена до feat(db,web): app-layer read-only D1 guard — getDb chokepoint над env.DB (#199) #225.

contracts.test.ts:137 остава отклонено, със същия аргумент: тестът твърди кой SQL предикат е излязъл (c.eu_funded = 1, substr(c.signed_at, 1, 4) IN), защото точно това е поведението на слоя — той не прави нищо друго освен да сглоби заявката. Тест, който не гледа SQL-а, не тества нищо тук.

Покритие след сливането

Workspace Редове Клонове Праг
apps/etl 100.00% 97.64% 100 / 97.6
apps/web 99.42% 95.96% 99.4 / 96
packages/config 100.00% 100.00% 100 / 100
packages/db 100.00% 98.12% 100 / 98.3
packages/ingest 100.00% 98.69% 100 / 98.7
packages/shared 98.87% 98.33% 98.8 / 98.3
packages/test-support 100.00% 100.00% 100 / 100

✅ No workspace dropped below its baseline (tolerance 0.5pp). Нито един праг не е свалян; apps/etl клони са вдигнати 97 → 97.6, за да не се губи реалната печалба.

Дребните минуси при db и web (−0.18pp / −0.04pp) идват от прясно слетия продуктов код (sql-ast-guard, report-schema, ConflictDetail, conflicts.ts), не от миграцията — тя не пипа продуктов код изобщо. Ако искаш ги затварям в отделен PR срещу main; тук биха размили дифа.

Няма продуктова промяна: 59 файла, всичките тестове, конфигурация за покритие и базовата линия. Като си готов — бутонът е при теб.

@todorkolev

Copy link
Copy Markdown
Collaborator

Бърза и добра работа - миграцията на 71-те двойника е точно каквото трябваше, а че строгостта на @sigma/test-support е хванала два реални пропуска (недекларираните четения на панела „последни договори" в getCompany/getAuthority, и {} as D1Database в eop/index тестовете) е най-доброто доказателство, че смяната си струва. CI е 4/4 зелено, MERGEABLE.

Четирите нишки, които предвидих, че се решават от общия двойник, наистина се решиха - проверих contracts.test.ts (binds вече се твърдят), препълването с 2 реда, единствения data_freshness маршрут и отделния sector_totals.

Две от останалите пет си струват още едно докосване, преди да го натиснем - и двете са секунди работа:

coverage-baseline.json - прагът lines: 100. Проверих fd2bc1b: пет workspace-а са на точно 100 при tolerance: 0.5. Това е нулев буфер в грешната посока - следващ PR, който добави дори един непокрит ред в който и да е от тях, пада под 99.5 и чупи check, включително PR-и, които изобщо не пипат тестове. Точно тази седмица минаха 6 PR-а под ratchet - един такъв червен build върху несвързана работа е скъп. Предложение: свали петте на 99 (или колкото е реалното покритие минус малък марж), за да е ratchet, а не капан. Останалите прагове (клонове ≥96 и т.н.) са добре.

config/src/index.test.ts:143 - коментарът още подвежда. Самата функция прави .trim() (виждаш го два реда по-горе - „trims surrounding whitespace"), тоест ' '.trim() е '', което е falsy и се хваща от if (!procedureType), точно както undefined/null. Коментарът обаче казва „' ' → truthy, so it PASSES if (!procedureType)" - описва път през кода, който не съществува. Тестът минава; проблемът е, че обяснението ще подведе следващия четец за поведението на guard-а. Един ред корекция.

Другите три (retry.test.ts реалното забавяне, search.suggest.tsx недовършената препратка, и config коментара по-горе) са изцяло козметика/бързина - ако предпочиташ да ги оставиш, кажи и ги приемам; само първата (coverage праговете) бих искал да е оправена, защото хапе несвързани хора.

Само тестове, не блокира свързаните лица. Кажи като си готов и минавам.

@lyubomir-bozhinov

Copy link
Copy Markdown
Collaborator Author

@todorkolev Благодаря. По четирите „козметични" — три от тях вече са направени на fd2bc1b; проверих ги отново поименно долу, за да не се въртим по нишки, които не се резолват. По праговете си прав в същината, но не за петте — и лекарството според мен е друго.

1. Праговете — измерено, не на око

Числата от fd2bc1b (колко непокрити реда трябват, за да падне заданието):

Workspace реда праг пада под непокрити реда до червено
packages/config 28 100 99.5% 1
packages/test-support 99 100 99.5% 1
apps/etl 169 100 99.5% 1
packages/ingest 470 100 99.5% 3
packages/db 1183 100 99.5% 6
apps/web 1391 99.4 98.9% 16

Тоест: прав си за три от петте, не за петте. При db трябват шест непокрити реда, при ingest три. Но при config, test-support и etl наистина един ред пали червено — приемам го без спор.

Причината обаче не е числото 100, а мерната единица. tolerance е процент. 0.5pp от 28 реда е 0.14 от ред — това не е малък буфер, това е никакъв буфер. Под ~200 елемента процентният праг спира да е ratchet и става капан, независимо дали е 100 или 99. При 99 config пада при два непокрити реда вместо при един; капанът остава, просто се мести с една педя.

Струва си да се каже и че капанът вече е на main, от преди този PR: packages/test-support е единственият workspace на main с lines: 100 / branches: 100 (от #331) — при 99 реда и 54 клона един непокрит ред или клон го пали. Този PR разширява експозицията от един workspace на пет; не я въвежда.

Затова не бих свалял праговете. Праг 99 върху покритие 100 не е ratchet — това е предварително разрешение то да падне: следващ PR може да остави ред непокрит в напълно тестван workspace и заданието остава зелено. Това е точно преместването на гредата, само отложено.

Предложение — оправи се механизмът, не числата. Буферът да е поне един елемент, независимо от размера:

// scripts/check-coverage.mjs, compareWorkspace()
const perItem = actual[metric].total > 0 ? 100 / actual[metric].total : 0;
const slack = Math.max(tolerance, perItem);
if (pct < base - slack - 1e-9) {

Прототипирах го и го измерих локално:

  • config → буфер 3.57pp, test-support → 1.01pp, etl → 0.59pp — един непокрит ред минава и в трите.
  • db, ingest, web — tolerance продължава да управлява, нищо не се променя (0.08pp/0.21pp/0.07pp per-item са под 0.5).
  • Гейтът остава зелен на fd2bc1b.
  • Един твой тест иска обновена фикстура: check-coverage.test.mjs:57 строи total: 100, тъй че спадът от 1pp там е точно един елемент и вече влиза в буфера. Фикстурата кодира точно допускането, което се оправя — не е отслабване на теста.

Това е твоят харнес и твоят слой. Готово е — кажи къде да влезе: отделен PR срещу main (по-чисто, ползва и test-support веднага), или да го сложа тук. Ако предпочиташ въпреки това да сваля праговете, кажи изрично и ще го направя — но записвам, че според мен това е грешната поправка.

2. config/src/index.test.ts — коментарът е верен, ето доказателството

.trim() не се случва преди пазача. Източникът на fd2bc1b, packages/config/src/index.ts:342:

export function procedureGroup(procedureType: string | null | undefined): ProcedureGroup {
  if (!procedureType) return PROCEDURE_UNKNOWN;                        // 343 — суровият низ
  return PROCEDURE_GROUP_BY_TYPE.get(procedureType.trim()) ?? PROCEDURE_UNKNOWN;  // 344 — trim-ът е тук
}

Пазачът гледа procedureType, не procedureType.trim(). Редът два реда по-горе, който си видял („trims surrounding whitespace"), е заглавие на тест, не ред от функцията.

Измерено, а не разсъждавано — временно смених ред 343 на return { key: 'HIT_THE_GUARD' } и извиках функцията:

"   "                  -> unknown          ← минава пазача, стига до .get('') и не уцелва
"\t\n"                 -> unknown          ← същото
undefined              -> HIT_THE_GUARD
null                   -> HIT_THE_GUARD
""                     -> HIT_THE_GUARD
"Открита процедура"    -> open

Двата пътя дават еднакъв резултат (unknown), затова е лесно за пропускане — и точно затова коментарът ги именува поотделно. Ако смяташ, че формулировката още може да подведе, дай текст и го слагам; но поведението, което описва, е това.

3. Другите две вече са направени


CI е 4/4 зелено на fd2bc1b, MERGEABLE, 0 назад от main. Единственото отворено е къде да влезе поправката на буфера — кажи и я подавам веднага.

Per @todorkolev on midt-bg#254: the five workspaces sitting at lines 100 drop to a
99 floor. Measured coverage is unchanged at 100% in all five — this only
widens the margin before the ratchet fires.

Why it mattered: `tolerance` is a percentage, so it is near-zero slack in a
small workspace. At 100/100 a single uncovered line red-builds
packages/config (28 lines), packages/test-support (99) and apps/etl (169) —
including on PRs that never touch tests. packages/db needed 6 and
packages/ingest 3.

Branch floors are untouched.
@lyubomir-bozhinov

Copy link
Copy Markdown
Collaborator Author

@todorkolev Направено — dece62d. Петте прага за редове са свалени на 99; измереното покритие си остава 100% и в петте, тъй че това е чист марж, не отстъпление. Праговете за клонове са непипнати.

apps/etl              100 → 99
packages/config       100 → 99
packages/db           100 → 99
packages/ingest       100 → 99
packages/test-support 100 → 99

CI е 4/4 зелено на dece62d, MERGEABLE, 0 назад от main.

Другите четири ги оставям както са — трите вече бяха направени на fd2bc1b (vi.useFakeTimers() на retry.test.ts:49, допълнената препратка в search.suggest.test.tsx, коментарът в config), а contracts.test.ts:137 остава отклонено със същия аргумент.

Една бележка за протокола, не за този PR: packages/config и packages/test-support са на клони точно 100 (18 и 54 клона), тоест същият капан важи и там — един непокрит клон ги пали. Ако искаш, свалям и тези две на 99 със същия коммит; кажи.

Твой е бутонът.

@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-ът

PR-ът е чисто тестово разширение, което вдига покритието до ≥95% за всички workspaces (надгражда #216). Промените са адитивни: нови тестови файлове в packages/db, packages/config, packages/ingest и др., плюс coverage-baseline.json и малка настройка в vitest.shared.ts, която изключва неизпълним код (фикстури, .d.ts, cloudflare stub-ове) от покритието. Няма промени в продукционен код.

Сигурност и злонамерен код

Чисто по всичките 5 партиди. Няма неочаквани мрежови извиквания (fetchFn/драйверите са vi.fn() или фалшиви D1 двойници), няма достъп до тайни/токени, нови зависимости, нито промени по auth/permissions/CI/build/workflow файлове. Няма опити за prompt injection — обяснителните коментари описват тествано поведение и доменен контекст, без да насочват присъдата на ревюъра.

Качество на тестовете

Високо. Mock-овете изолират D1/мрежата коректно, използват се route-less двойници, които хвърлят при неочаквани заявки. Асерциите са конкретни (не тавтологични) и упражняват смислени клонове: security-инвариантите на assertReadOnlySelect, маскирането на D1 грешки в run_sql, драйвер-fallback-ите, keyset пагинацията напред/назад, CSV стрийминга през CHUNK границата, scoped DELETE ... WHERE source = ? chunk логиката в staging, YoY/trend клоновете, narrowing-а на registry_role и таксономичните инварианти в packages/config. Проверената аритметика (staged: 28, computeWorkerCatchupPlan, EU дял, конверсиите по пега, границите 100/101/250) е вярна. format.test.ts и staging.test.ts са сверени спрямо реалните имплементации.

Незначителни забележки (не блокиращи)

  • Няколко нови твърдения са по-слаби, отколкото имената им загатват (маркирани инлайн като предложения).
  • Дребен въпрос по coverage-baseline.json.
  • Част от асерциите разчитат на toContain върху сурови SQL фрагменти, което ги прави чупливи при бъдещ рефактор — но следва вече установената конвенция в тези файлове.

Заключение

Няма блокиращи забележки. Адитивно, добре документирано разширяване на покритието. Одобрявам (COMMENT).

Comment thread apps/web/app/components/ConflictDetail.test.tsx Outdated
Comment thread apps/web/app/components/ConflictDetail.test.tsx Outdated
Comment thread coverage-baseline.json
Both findings are @ydimitrof's on midt-bg#254, and both were real.

- the source-URL test selected `.cc-source, .conflict-detail`. There is no
  `.cc-source` in the component — the stat cells carry no per-field class — so
  it always widened to the whole card, where `toContain('—')` can be satisfied
  by any other dash. Now pinned to the „Източник" cell via its `<dt>`, with a
  positive control asserting the same cell holds the declaration link when the
  URL is present, so a broken finder cannot make the negative case pass.
- the in/out-window test asserted only that both rows render and both numbers
  appear, which holds with the split broken in either direction. Now asserts
  the per-row `contract-item-conflict` modifier and that the outside row sits
  behind the „Извън периода" disclosure.

Mutation-verified: forcing every row to carry the modifier, forcing none to,
and deleting the source-cell fallback each fail the suite.
@lyubomir-bozhinov

Copy link
Copy Markdown
Collaborator Author

@ydimitrof Благодаря — и двете бележки по тестовете бяха верни, и двете са поправени в 436e1b3. Третата е с друга причина, отколкото предполагаш.

ConflictDetail.test.tsx:121 — селекторът. Прав си, и е по-зле, отколкото го описваш: .cc-source не съществува в компонента. Клетките са .cc-stat без клас за конкретното поле, тъй че '.cc-source, .conflict-detail' винаги падаше на цялата карта — не само „при липсваща клетка". Твърдението беше toContain('—') върху цялата карта, тоест всеки друг тирета го задоволяваше.

Пристегнато към клетката „Източник", намерена по нейния <dt>, плюс позитивен контрол: същата клетка трябва да съдържа връзката „декларация", когато URL-ът го има. Без него счупен finder би направил отрицателния случай зелен по случайност.

ConflictDetail.test.tsx:205 — модификаторът. Също прав. Сега твърди contract-item-conflict поред: редът Д-IN го носи, Д-OUT не, и извънпериодният ред стои зад disclosure-а „Извън периода".

Проверено с мутации, не на око — всяка от трите чупи сюитата:

мутация резултат
всеки ред получава contract-item-conflict ❌ пада
нито един ред не го получава ❌ пада
fallback-ът „—" в клетката „Източник" е премахнат ❌ пада

coverage-baseline.json:29 — причината е друга. packages/test-support мери 100.00% редове на текущия HEAD; няма непокрит код и throwingD1 е покрит. Прагът е свален на 99 по изрична заявка на @todorkolev като условие за merge — коментарът му и моят отговор с числата. Накратко: tolerance е процент, тъй че при 28-редов workspace 0.5pp е 0.14 от ред — един непокрит ред пали червено на config (28), test-support (99) и etl (169), включително на PR-и, които не пипат тестове. Аз бях против свалянето по същата причина, която изтъкваш, и предложих поправка на механизма вместо на числата (slack = max(tolerance, 100 / total)); Тодор избра свалянето. Съгласен съм с инстинкта ти — но занижаването не е защото покритието е паднало.

Изравнено и с main (#335). CI е 4/4 зелено на 436e1b3.

Едно процедурно: и четирите ти прегледа са подадени като COMMENT, тъй че PR-ът още няма нито едно одобрение и не може да влезе, макар заключението ти да е „няма блокиращи забележки, одобрявам". Ако вердиктът стои — подай го като GitHub Approve и Тодор може да го мине.

@lyubomir-bozhinov

Copy link
Copy Markdown
Collaborator Author

@todorkolev Готов е — всичко, което поиска, е направено.

  • Твоето условие (праговете за редове) — свалени на 99 в dece62d.
  • Бележките на @ydimitrof от 31.08 — двете по тестовете са поправени в 436e1b3, проверени с мутации; третата (test-support 100 → 99) е обяснена: покритието е 100%, свалянето е по твоята заявка, не заради непокрит код. Заключението на прегледа му е „няма блокиращи забележки, одобрявам".
  • Изравнен с main (fix(cacbg): ретрай на четенето на броевете след ship, не фалшив провал #335), 0 коммита назад.
  • CI 4/4 зелено на 436e1b3, MERGEABLE.

Нула промени в продукционен код — само тестове, конфигурация за покритие и базовата линия. Ще го минеш ли?

@todorkolev

Copy link
Copy Markdown
Collaborator

Одит на 34-те неразрешени нишки — по същество са затворени, само не са маркирани

@lyubomir-bozhinov, @ydimitrof — PR-ът стои BLOCKED заради неразрешени нишки (същото се случи и на #337: разрешаването им го върна на CLEAN веднага). Понеже 29 от тях са outdated, а това означава само че кодът е мръднал — не че забележката е взета предвид — минах през всяка от 34-те срещу текущия код на 436e1b3.

Резултат: 22 addressed, 1 declined с основание, което издържа, 1 obsolete, 10 отворени — от последните 3 са само стил, 2 са наследени от main, а не внесени тук.

Присъда по всяка нишка
# path verdict evidence
T1 apps/web/app/components/ScrollToTop.test.tsx ADDRESSED The original rAF is saved at :29,37 and restored in afterEach at :45-51; the override at :103 cannot leak.
T2 apps/web/app/lib/riskLogic.test.ts ADDRESSED The name and comment now correctly describe bidsReceived: null and the failed != null guard at :37-40.
T3 packages/config/src/index.test.ts ADDRESSED A non-empty unknown value exercises the real map miss at :59-64; whitespace versus nullish paths are correctly explained and asserted at :147-158.
T4 packages/db/src/queries/contracts.test.ts ADDRESSED Tests now use the recording fake at :2 and assert a captured bind at :230; recording is implemented in packages/test-support/src/fake-d1.ts:139-141.
T5 packages/db/src/queries/contracts.test.ts NOT-ADDRESSED The filter test still asserts implementation-specific SQL fragments verbatim at :148-157, including both examples from the review.
T6 packages/db/src/queries/contracts.test.ts ADDRESSED The explicit limit is checked as LIMIT ? and bind [5] at :226-230; the default [10] is checked at :232-235.
T7 pnpm-lock.yaml NOT-ADDRESSED Coverage-v8 still resolves as 4.1.10(vitest@4.1.7) at :29-31, while its exact peer is vitest: 4.1.10 at :1269-1273.
T8 scripts/check-coverage.mjs NOT-ADDRESSED readdirSync(join(ROOT, group)) remains unguarded by an existence check at :231-235.
T9 scripts/check-coverage.mjs NOT-ADDRESSED Workspace manifests are still passed directly to JSON.parse without contextual error handling at :236-239.
T10 vitest.shared.ts OBSOLETE Vitest 4 removed coverage.all; coverage.include is now the supported way to include uncovered files, and HEAD supplies it at :11-13. See the official Vitest migration guide.
T11 .github/workflows/ci.yml ADDRESSED The SHA at :103 is the verified official actions/upload-artifact v7.0.1 release.
T12 .github/workflows/ci.yml ADDRESSED The SHA at :144 is the verified official actions/download-artifact v8.0.1 release.
T13 .github/workflows/ci.yml NOT-ADDRESSED :165 and :169 still submit the entire file with no length check, truncation, or artifact-link fallback.
T14 package.json NOT-ADDRESSED @vitest/coverage-v8 still precedes @types/node at :33-34. This is only a style nit; the configured Prettier check accepts it.
T15 apps/web/app/lib/retry.test.ts ADDRESSED Fake timers are enabled at :48-49, advanced at :59-60, and restored at :69.
T16 apps/etl/src/eop.test.ts ADDRESSED fakeDbFromFreshness registers only data_freshness at :102-106; unmatched raw-staging queries now throw through the shared fake.
T17 package.json NOT-ADDRESSED Duplicate of T14: the ordering at :33-34 remains unchanged.
T18 pnpm-lock.yaml NOT-ADDRESSED The unsupported exact-peer pairing remains at :29-31 and :1269-1273.
T19 turbo.json ADDRESSED CI always invokes tests with --coverage at .github/workflows/ci.yml:90-100; moreover, Turbo includes pass-through arguments in task hashes, so covered and uncovered runs cannot share this cache entry. See Turbo’s caching documentation.
T20 .github/workflows/ci.yml ADDRESSED As with T11, :103 pins the exact verified official v7.0.1 commit.
T21 .github/workflows/ci.yml ADDRESSED Workspace names are restricted to safe characters at scripts/check-coverage.mjs:41-44, rejected at :180-183, and rendered coverage values are numeric toFixed() output at :125-127.
T22 package.json NOT-ADDRESSED Both packages are 4.x at :33,39, satisfying the substantive major-version check, but the secondary ordering request remains unmet at :33-34.
T23 coverage-baseline.json ADDRESSED Despite the earlier refusal, HEAD now lowers this line floor to 99 at :5, with tolerance retained at :2.
T24 vitest.shared.ts ADDRESSED The PR description discloses the exclusions; HEAD also documents what leaves the denominator and why at :21-40.
T25 vitest.shared.ts ADDRESSED The JSON and Markdown exclusions are gone; :23-25 explains why they are unnecessary, and the complete list ends at :41.
T26 apps/web/app/routes/search.suggest.test.tsx ADDRESSED The placeholder now identifies #225 and its connection to #199 at :6-8.
T27 packages/db/src/queries/competition.test.ts DECLINED-WITH-REASON The requested 999 → 50 clamp is not product behavior: competition.ts:427 implements an exact-50/default-20 toggle. The corrected test documents and exercises that behavior at :277-286.
T28 vitest.shared.ts ADDRESSED The broad **/src/test/** exclusion was replaced by two named test stubs, with the denominator risk documented at :34-40.
T29 packages/db/src/queries/trend.test.ts ADDRESSED The fake now routes FROM sector_totals separately to [] at :166-177, rather than returning series rows for every query.
T30 apps/web/app/lib/assistant/render-format.test.ts ADDRESSED The null result is pinned to literal —, and present/unparseable inputs have concrete expectations at :52-64.
T31 vitest.shared.ts ADDRESSED No directory-wide src/test exclusion remains; only the two ETL stubs are named at :28-40. The former SQLite shim is now measured under packages/test-support.
T32 apps/web/app/components/ConflictDetail.test.tsx ADDRESSED :223-245 asserts that only Д-IN has contract-item-conflict and that Д-OUT is in the outside-period disclosure.
T33 apps/web/app/components/ConflictDetail.test.tsx ADDRESSED statValue() finds the exact labelled stat cell or throws at :96-109; the source-cell assertions and positive control are at :135-150.
T34 coverage-baseline.json NOT-ADDRESSED The packages/test-support line floor remains 99 at :28-30, although throwingD1 now has direct tests at packages/test-support/src/fake-d1.test.ts:397-433; the requested 100 floor was not restored.

Остатъчни проблеми

  • Medium — coverage-tool compatibility: T7/T18 remain real. The lock installs coverage-v8 4.1.10 against Vitest 4.1.7 despite an exact 4.1.10 peer. This does not prove the present percentages are wrong, but it leaves the coverage runner on an unsupported pairing. This is inherited from current main, not introduced by the net PR diff.

  • Low — incomplete behavioral protection for contract filters: T5 remains. The comprehensive filter test at contracts.test.ts:132-158 checks SQL text but not the corresponding bound values/order. The fake can now record binds, but only the unrelated limit binds are asserted at :230,235.

  • Low — weakened test-support ratchet: T34 leaves the floor at 99; with tolerance 0.5, CI permits 98.5. This is an explicit policy relaxation, not hidden code or a false current measurement, and it remains well above the PR’s ≥95% goal.

No current exclusion hides product code, and the reviewed vacuous/tautological assertions have been corrected. T8/T9/T13 are fail-closed robustness or future-scaling concerns; T14/T17/T22 are style-only.

Две уточнения от моя страна, проверени независимо:

  • T7/T18 не са на този PR. @vitest/coverage-v8@4.1.10(vitest@4.1.7) стои идентично в main и тук — не е внесено от диффа. Заслужава отделен follow-up; Dependabot вече ще го предлага (4.1.11 съществува и изравнява двойката).
  • T23 / T34 — едно разминаване, което си струва да се каже на глас. В нишката пише „ще откажа, съзнателно — праговете вървят само нагоре", но в главата прагът на packages/test-support все пак пада 100 → 99. Това е единственият свален праг; останалите шест скачат рязко (etl 74→99, ingest 86.3→99, config 92.8→99, db 94.5→99, web 91→99.4). Ако свалянето е нарочно, струва си един ред обосновка в нишката; ако е недоглеждане — връщането на 100 е едно число.

Проверка срещу днешния main

Слях PR-а върху 15818e7 (след #336, #337, #339, #340, #341) и го пуснах под Node 24:

конфликти 0
typecheck 8/8
тестове web 648, db 647, scripts 148/148
coverage ratchet ✅ „No workspace dropped below its baseline"

Нищо не крие продукционен код и не останаха тавтологични твърдения — точно те бяха поправени. Няма блокиращи забележки.

Практически: PR-ът е BEHIND (fork), тъй че иска и „Update branch". Аз не мога да го направя — токенът ми няма права над fork-а.

🤖 Generated with Claude Code

https://claude.ai/code/session_013pBk7JaPgpsgELYGZgnkwt

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.

4 participants