feat(cacbg): печат за пълнота - отрязан корпус не бива да се публикува - #334
Conversation
#313 направи частичния корпус ОЧАКВАНО състояние: краулът вече спира сам на срока и запазва каквото е събрал, което е и причината студен корпус да завършва за два хода. Но `restore-keys: cacbg-raw-` връща най-СКОРОШНИЯ запис, не най-пълния, а extract.mjs изброява файловете с readdirSync, без изобщо да ги сверява с описа в list.xml. Тоест отрязан корпус просто даваше по-малка публикувана повърхност, без грешка никъде. Нито един от съществуващите гейтове не го хваща: гейтът за монотонност вижда нетен ръст, щом новите връзки надвишат загубените; подът --min-links само брои; а първо пускане в нова среда няма никаква база за сравнение. Печатът се пише САМО когато корпусът се сверява с описа, и се чисти в началото на всяко обхождане - така всеки ненормален изход (спиране по срок, прекъсвач, необработена грешка, убит изпълнител) оставя корпуса неподпечатан, без да му трябва собствен път за почистване. Гейтът е при четенето: extract.mjs отказва без печат и назовава --allow-partial-corpus. Значението е разделено на две - частичният кеш остава напълно годен за ПРОДЪЛЖАВАНЕ (следващият крал прескача каквото е на диска), но негоден за ПУБЛИКУВАНЕ. --allow-incomplete не подпечатва. Този флаг записва, че оператор е ВИДЯЛ недостига и го е приел; приемането е негово и не пътува към следващ ненаблюдаван ход. Открито от тест, не по замисъл - първата версия подпечатваше, защото стигаше до същия ред. extract.mjs получава CACBG_RAW/CACBG_STAGING по модела на load.mjs, за да е тестваем; по подразбиране сочи истинския scratch. Тестове: 6 нови, 313 в целия cacbg+tr пакет. Убиват три мутанта - печат без проверка на присъдата, липсващо чистене в началото, махнат гейт при четенето.
Test coverage
✅ No workspace dropped below its baseline (tolerance 0.5pp). 📈 Coverage rose by more than 1pp — run |
Първата версия на печата имаше преходен капан от същия вид, който #332 и #333 вече ни удари: всички съществуващи кешове са отпреди печата, тоест ход без обхождане (понеделнишкият cron) щеше да откаже на extract - не защото корпусът е отрязан, а защото е СТАР. И поправката щеше да зависи от поредността "слей, после пусни full_crawl=true преди понеделник". Вместо това ходът се лекува сам. Нова стъпка след възстановяването проверява печата; краулът тръгва при full_crawl ИЛИ при липсващ печат. Върху пълен кеш това е ~2 минути (файловете на диска се прескачат) и сверява срещу живия регистър - авторитетния източник, а не евристика върху това какво има на диска. Запазването и проверката на кеша следват същото условие, за да не се изгуби довършеното. Стабилно състояние с печатан кеш остава без мрежа, по замисъла на графика: лекуването се задейства само за кеш, който никога не е бил потвърден цял - точно случаят, в който довярването му е бъгът. Гейтът в extract.mjs остава непроменен и вече означава друго: всички пътища дотам гарантират печат, значи задействането му е непредвидено състояние, не позната преходност. Fail-closed е верният отговор за това.
Ревюто на Codex намери три блокера и три сериозни в печата, всичките от една порода: сертификатът можеше да се издаде за корпус, който никой не е сверил целия. - краул с --folders подмножество печаташе цялото дърво: завършено подмножество не е завършен корпус. Печатът вече иска пълно откриване; подмножеството пак чисти стар печат (мутира корпуса), тоест оставя възобновимо-но-непубликуемо състояние. - индекс, който открива нула папки, минаваше за тривиално пълен (възпроизведено: изход 0 и печат folders: 0 върху съществуващо дърво). Вече е провал: регистърът публикува набори от 2015 г. без прекъсване, празен индекс е счупена страница, не реално състояние. - страница за поддръжка (HTTP 200, парсва се до нула реда) занулява announced и презаписва кеширания list.xml, който extract чете. При файлове на диска, които ѝ противоречат, папката се прескача - корпусът става непълен, без печат, кешираният опис оцелява. - разархивирането на кеша не е атомарно: провалено извличане може да остави печата без XML-ите, при неотчетен ключ. Стъпката за печата вече изхвърля цялото дърво при празен cache-matched-key - състоянието става точно "няма кеш", което лекуващият краул поема. - CACBG_RAW/CACBG_STAGING заобикаляха PII предпазителя: override-ът пренасочва истинския изход, а assertScratchIgnored проверява само фиксирания scratch/. Нов assertOverrideDirSafe прилага същото правило към override пътя: извън хранилището - свободно; вътре - само ако git го игнорира. Тестове: 11 (5 нови), убиват петте мутанта по горните поправки; целият cacbg+tr пакет 318. Позитивният extract тест вече твърди и изход 0, а --allow-partial-corpus има собствен тест - двата оцелели мутанта от ревюто. Извън обхвата, отбелязано за отделни issue-та: "obtained" значи "файлът съществува", не "валидна декларация" (заварено), и студен кеш в понеделник пак дава червен ход по замисъл - лекуването довършва, не гарантира зелено на всяка цена.
Втората проверка на Codex потвърди 1-3 поправени, но възпроизведе два остатъчни пробива и четири оцелели мутанта. Свежата папка (находка 4 докрай): страница за поддръжка върху папка БЕЗ файлове на диска зануляваше announced и яздеше валиден съсед до печат - възпроизведено: skippedSets 0, incomplete false, печат. Нула реда от 200 отговор вече е прескачане БЕЗУСЛОВНО: регистърът индексира папка чак когато има декларации, тоест реално празен набор не е наблюдаван; ако някога се появи, ходът става червен за оператор - вярната посока на провал за сертификатор. Предпазителят на override пътищата (находка 6 докрай): лексикалното startsWith пропускаше корена на хранилището (равенство), /proc/self/cwd псевдоним, чужд git worktree и главни/малки букви. Вече пита самия git: rev-parse --show-toplevel от най-дълбокия съществуващ предшественик (операционната система резолвва symlink-ове при chdir), после check-ignore в НАМЕРЕНОТО хранилище, чието и да е то. Извън всякакъв worktree - свободно; корен на worktree - отказ. Мутантите: подмножество и празен индекс вече започват от посаден стар печат (чистенето трябва да е безусловно и ПРЕДИ проверката за празно); CACBG_RAW има собствен тест огледално на CACBG_STAGING; условието announced > 0 е документирано като недостижима защита в дълбочина - мутант, който го трие, оцелява нарочно, вместо тест да кодира невъзможен сценарий. Тестове: 13, трите наименувани мутанта падат; целият пакет 320.
… вяра Третата проверка на Codex потвърди кръг 2 и намери три нови пробива. Осакатен XML (възпроизведено): тяло, срязано по средата, чийто оцелял цял <Declaration> се брои за цялото обявление - announced 1, сверено, ПЕЧАТАНО. XMLValidator проверява структурата преди броене и кеширане; невалиден документ = прескочена папка. Свиване на списъка (възпроизведено): кеширано [a1,a2], входящо [a1] - по-пълният кеш се презаписваше и свиването се заверяваше. Дизайнът на продължаването стъпва на неизменимост по година, тоест свиване е противоречие: папката се прескача, кешът оцелява, изтегляне на декларация е човешко решение върху червен ход. Предпазителят на override пътищата, три поправки: - git грешка вече не значи пропуск. Само потвърдено "not a repository" минава; EPERM, липсващ git и повредено хранилище отказват (били са PASS). Тестът с PATH=/nonexistent го приковава. - наследени GIT_DIR/GIT_WORK_TREE се изчистват - иначе подменяха политиката на друго хранилище (възпроизведено с exclude-всичко декой). - проследеното бие игнорираното: файл, влязъл в индекса на ВЪНШНОТО хранилище преди вложеното да съществува, остава записваем, а check-ignore го покрива с имплицитния ignore на вложени хранилища. ls-files по всяко обхващащо хранилище отказва при съвпадение. Отделно: вложено хранилище БЕЗ нищо проследено отвън законно минава - git add отказва през границата, тоест наистина не е записваемо. Отстрани: първата ми версия на ls-files проверката хвърляше отказа ВЪТРЕ в try и собственият catch я преобличаше в "ls-files failed" - хванато от твърдението за съобщението в теста, точно затова то твърди и текста, не само кода на изход. Тестове: 18, седемте мутанта по кръга падат; целият пакет 325.
|
Прегледах го адверсариално при
18-те теста в Едно операционно следствие за протокола (не проблем, по дизайн): ръчен |
Четвъртата проверка потвърди свиването и вложените хранилища, и намери четири нови, всичките поправени и приковани с тестове. Многокоренов документ (блокер, възпроизведен): XMLValidator приема <wrong/><root>...</root>, а снизходителният парсер слива корените и parseList пак намира редовете. Валидацията вече изисква и точно един документен елемент. Pathspec инжекция (блокер, проверен): -- НЕ изключва магията на git, а check-ignore отговаря 0 за директория, буквално кръстена :(top)scratch/... - съди се игнорираният резолвнат път, а записваемото литерално име минава. --literal-pathspecs е неизползваем (check-ignore отхвърля literal магията, докато изпълнява :(top) - проверено), но ./ префиксът пази името име и в двете команди, в двете посоки. Локализиран git: машината върви на bg_BG и "not a git repository" пристига преведено - английското съвпадение четеше законен /tmp override като необясним провал и отказваше. gitEnv() заковава LC_ALL=C. GIT_DIR по подразбиращия се път: assertOverrideDirSafe чистеше GIT_*, но assertScratchIgnored ги наследяваше - декой хранилище можеше да отговаря вместо истинското и по подразбиращата се релса. Двете релси вече делят gitEnv(). Фикстурата сочи работното дърво на декоя към самия декой - иначе .gitignore се чете от истинското дърво и подмяната е невидима. Тестове: 22, четирите мутанта по кръга падат; целият пакет 329.
…ден кеш Финалното съобщение на четвъртия кръг беше срязано от филтъра на самия Codex; от лога възстанових още два блокера и една дребна, всичките поправени. Symlink целта не се канонизираше: scratch/ е игнориран, тоест symlink scratch/cacbg/leak -> scripts/ минаваше лексикалната проверка, докато извличането пише ПРЕЗ него в проследени файлове. Предпазителят вече резолвва най-дълбокия съществуващ предшественик преди да съди - резолвнатото място е scripts/, проследено, отказ. Границите на файлови системи: изчистването на GIT_* маха и GIT_DISCOVERY_ACROSS_FILESYSTEM, при което mount точка ВЪТРЕ във външно работно дърво отговаря "not a repository", макар дървото да може да проследява пътя. Променливата се връща изрично след изчистването. Нетестваемо без root (bind mount) - редът носи обяснението вместо фикстура. Повреден кеширан списък (DOCTYPE наследство, срязан запис) сриваше shrink проверката, вместо валидният входящ списък да го излекува. Нечетимият кеш се брои за нула редове: без възражение за свиване, а atomicWrite го заменя със здравия. Тестове: 24, двата тестваеми мутанта падат; целият пакет 331.
Петата проверка потвърди седемте поправки от кръг 4 и остави два "блокера". Единият е реален, другият - не. Реалният (блокер 2): само override пътищата минаваха през assertOverrideDirSafe, който канонизира и пита git. Подразбиращите се raw/staging разчитаха на assertScratchIgnored, който проверява само фиксирания .probe - symlink НА самата подразбираща се позиция, сочещ към проследена територия, му е невидим, докато I/O го следва. Вече и двете подразбиращи се позиции минават канонизиращия предпазител безусловно, и в extract.mjs, и в краула в fetch.mjs. Отхвърленият (блокер 1): твърдеше, че ./ префиксът спира двоеточие- магията, но оставя обикновените глоб-метазнаци активни, тоест буквална дир, чието име е глоб-образец, скрива проследени descendants от ls-files. Проверено чрез изпълнение, не по документация: дир, буквално кръстена 'a*b', 'a?b' и 'a[x]b', с проследен файл вътре - ls-files -- './name' намери файла и в трите. Метазнаците се третират буквално, защото пътят сочи съществуваща директория с точно това име. Рецензентът не можа да пусне тестове (read-only монтиране) и разсъждава теоретично; изпълнението го опровергава. Follow-ups от кръга (не блокери, за отделно issue): броене на top-level елементите директно вместо по разчетени ключове; структурна валидация на кеширания списък преди да му се вярва броят; bind-mount регресия, когато CI я поддържа. Тестове: 25, мутантът по блокер 2 пада; целият пакет 332.
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.
Какво
Печат за пълнота до суровия корпус.
fetch.mjsго пише само когато корпусът се сверява с описа нарегистъра, и го чисти в началото на всяко обхождане.
extract.mjsотказва да извлича без него.Затваря дупката, отбелязана при ревюто на #313, но неподадена като issue.
Защо
#313 направи частичния корпус очаквано състояние - краулът спира сам на срока и запазва каквото има,
което е и причината студен корпус да завършва за два хода вместо никога. Но:
restore-keys: cacbg-raw-връща най-скорошния запис, не най-пълния - няма как да ги различи;extract.mjsизброява файловете сreaddirSyncи никога не ги сверява сlist.xml, макар да четеописа за друго (контекста на лицето).
Тоест отрязан корпус даваше по-малка публикувана повърхност, без грешка никъде.
Конкретният път: ход с
full_crawl=trueудря срока на 29 от 37 набора и запазва частичен кеш → месечниятcron се задейства с
full_crawlпразно (тоест false, по замисъл) → възстановява се най-скорошният кеш,частичният → повърхността се преизчислява без 2015-2019 и се публикува.
Нито един съществуващ гейт не го хваща. Гейтът за монотонност сравнява с предишната повърхност и вижда
нетен ръст, щом новите връзки надвишат загубените - точно каквото стана вчера (103 → 277); тихото
недопубликуване се крие вътре. Подът
--min-links 250брои, тоест отрязан корпус с 260 връзки минава. Апърво пускане в нова среда няма база за сравнение изобщо - гейтът сам казва "first run" и пропуска.
Поправката
Пишещата страна. Печатът се чисти ПРЕДИ обхождането и се пише само при сверена присъда. Чистенето в
началото значи, че всеки ненормален изход - спиране по срок, прекъсвач, необработена грешка, убит
изпълнител - оставя корпуса неподпечатан, без да му трябва собствен път за почистване.
Четящата страна.
extract.mjsотказва без печат и назовава--allow-partial-corpus. Значението еразделено на две: частичният кеш остава напълно годен за продължаване (следващият крал прескача
каквото е на диска), но негоден за публикуване.
--allow-incompleteне подпечатва. Този флаг записва, че оператор е ВИДЯЛ недостига и го е приел -приемането е негово и не бива да пътува към следващ ненаблюдаван ход. Това го открих от тест, не по
замисъл: първата версия подпечатваше, защото просто стигаше до същия ред.
extract.mjsполучаваCACBG_RAW/CACBG_STAGINGпо модела наload.mjs, за да е тестваем; поподразбиране сочи истинския scratch, тоест поведението не се променя.
Тестове
Шест нови, покриват двете страни: подпечатване при сверен корпус, липса на печат при спиране по срок,
липса при
--allow-incomplete, изчистване на стар печат при провалено обхождане, отказ при четене, ипреминаване при подпечатан корпус.
Убиват три мутанта:
extract.mjsЦелият пакет
scripts/cacbg+scripts/tr: 313 минават.Ефект върху сегашния конвейер
Никакъв при нормален ход - краулът и извличането са в едно пускане, тоест печатът е пресен. Промяната се
усеща само там, където и трябва: ход, който НЕ обхожда, върху кеш от недовършено обхождане.