Skip to content

Latest commit

 

History

History
168 lines (146 loc) · 18.4 KB

File metadata and controls

168 lines (146 loc) · 18.4 KB

LESSONS — уроки сборки factgate (2026-07-03)

Уроки ЭТОЙ сборки. Не дублирует досье (../LESSONS.md — 26 правил + мета-уроки бенчей).

NaN-сравнения молча выключают гейт — флаги валидируются на входе

Fresh-context ревьюер поймал: --today кривая-датаInvalid Date → каждое сравнение возраста = false → OUT-OF-WINDOW никогда не срабатывает → протухший отчёт получает PASS. Гейт, который на мусорном входе не падает, а пропускает, — хуже отсутствия гейта (даёт ложный сертификат). Фикс: невалидный --today/--window-days/--max-age-days = exit 2. Урок общий: у детерминистического гейта параметры — тоже trust boundary.

404 ≠ доказательство фабрикации, если API видит не всё

NVD 404 честно опровергает CVE (база полная). GitHub keyless API 404-ит и приватные репо — валить отчёт «FABRICATED REPO» нельзя, только warning «confirm manually». Урок: сила вердикта гейта = полнота источника. Fetch-or-omit работает только против источника, который позитивно опровергает.

Gemini-TOML drift был ложноположительным ещё в v4.1

Drift-чек сравнивал hash тела SKILL.md с hash всего установленного TOML (тело обёрнуто в шаблон) → вечный «DRIFTED». Чинится сравнением с регенерированным выхлопом шаблона (tmpl.replace('__BODY__', body)), не с телом. Урок: drift-чек обязан сравнивать «что инсталлятор записал бы сейчас», а не полуфабрикат.

hermes-таргет пишет в ~ даже в «project»-режиме

В manifest у hermes и project, и global указывают в ~/.hermes/.... Тестовая установка --harness all из песочницы всё равно тронула реальный домашний каталог. Урок: перед тестом инсталлятора смотреть, какие таргеты абсолютные/домашние; тест «в песочнице» не изолирует харнессы с ~-таргетами.

Keyless GitHub API умирает об rate-limit прямо в демо

api.github.com без токена = 60 req/h на IP; демо-прогон словил 403 с первого раза. Любой новый живой источник гейта обязан иметь явную деградацию (403/сеть → warning, не failure) и демо, которое не падает на rate-limit — иначе гейт начнёт «ловить фабрикации», которых нет. (NVD/OSV щедрее, но retry в getJson тоже неспроста.)

Header-aware парсер требует регрессионной фикстуры в --demo

Фикс «quote = самая длинная ячейка» невозможно проверить глазами: старый и новый парсер дают одинаковый результат на коротких claim. В демо добавлена строка с claim длиннее quote — v4.1-эвристика на ней падает, header-aware проходит. Урок: фикс парсера без фикстуры, ломающейся на старом коде, — не фикс.

Инлайновый node -e с бэктиками в теле патча — ловушка

Патч-строки с ` внутри template literal ломают eval; PowerShell 5.1 к тому же пишет UTF-16/BOM через Set-Content. Урок: правки файлов с юникодом/бэктиками — только отдельным .mjs-скриптом в scratchpad (Write → node script.mjs), не однострочником.

Проверяй живьём эндпоинты, которые вписываешь в собственную доку

fresh.md изначально обещал Reddit public JSON как keyless-floor. Живой чек при сборке: www.reddit.com/search.json → 429, old.reddit.com → 403 с этой машины при любом UA. HN Algolia — работает. Дока переписана честно (Reddit через web_search site:reddit.com

  • фетч old.reddit-пермалинка). Урок: fetch-or-omit применяется и к инструкциям скила — непроверенный эндпоинт в доке = та же галлюцинация, просто отложенная.

Drift-чек, который смотрит один файл из 15, — не drift-чек

v4.1 --drift хэшировал только тело SKILL.md; правка modes/*.md или verify.mjs оставалась невидимой. v5 сравнивает каждый установленный файл с исходником (SKILL.md — по телу, остальные — целиком). Проверено: правка modes/fresh.md ловится, после переустановки чисто.

Гейт не должен наказывать за честность (e2e-находка Opus-прогона)

OSV-скан бил ЛЮБОЙ pkg@x.y.z в отчёте — включая «avoid ws@8.17.0» и дословную цитату NVD «fixed in ws@8.17.1». Писатель на Opus был вынужден калечить verbatim-цитату advisory, чтобы пройти гейт — прямой конфликт с правилом 1 (цитируй дословно). Фикс детерминистский: уязвимая версия, каждая строка с которой помечает её как уязвимую/исправленную → warning; немаркированная строка (рекомендация в прозе или npm install … в коде) → по-прежнему FAIL. Урок: у каждого гейта есть второй режим отказа — false positive, который учит модель врать формой; ловится только реальным e2e-прогоном, не демо-фикстурами.

Живое доказательство тезиса: 71 agentic-голос пропустил, гейт завалил (e2e движка)

Реальный Workflow-прогон fanout.js (99 агентов, «Bun production-ready?»): adversarial- верификация подтвердила 11 claims, а bridge→verify.mjs завалил 5 из 6 сэмплированных цитат — extract-стадия на Sonnet сшивала эллипсисами и перефразировала («The team successfully migrated…» вместо реального «We're running 4 of 7 services…»; «approximately 4,700» вместо «around 4.7k» со страницы). Голосующие агенты этого не ловят — они судят claim, не substring. Два фикса: (1) FETCH_PROMPT движка теперь требует VERBATIM contiguous span и объявляет, что цитату будет substring-матчить машина; (2) отработан документированный цикл «FAIL → достать реальные спаны с живых страниц → re-gate»: после ремонта 6/6 supported, PASS. Урок = смысл проекта, пойманный за руку: модельная проверка ≠ детерминизм. И честная пометка: prompt-фикс экстрактора без повторного полного прогона не считается перепроверенным — нужен re-benchmark.

Нормализация текста — часть контракта гейта (e2e-находка fresh-прогона)

norm() затирал ВСЕ HTML-entities пробелом → ' (так HN кодирует апостроф) превращал «could've» на странице в «could ve», и честная дословная цитата ложно валилась как UNSUPPORTED. Фикс: частые текстовые entities (' " & < >   и кудрявые) декодируются ДО бланкинга остальных; фикстура в демо. Смежные доки-находки того же прогона: цитируешь коммент — ссылайся на permalink коммента (родительский тред пагинируется), Algolia-сниппет ≠ цитата (verified: no), окно меряется от даты запуска гейта строгим >. Урок: quote-in-page живёт или умирает на нормализации — она такой же контракт, как схема леджера, и требует фикстур.

Дорог не интеллект, а спавн: оверхед агента доминирует над работой (бенчмарк 2026-07-02)

Замер по транскриптам: 71 голос-агент = 5.1MB, 15 extract-агентов с инлайн-текстом = 0.7MB. Каждый спавн платит системный промпт + схемы тулзов + свой WebSearch — сама «работа» дешевле обвязки. Следствия, по убыванию силы: (1) не спавнить вообще (линейный путь: один контекст + zero-token скрипты, модель только выбирает предложения-кандидаты по ID и синтезирует); (2) батчить суждения (1 судья на 8 claims вместо 3 голосов на claim); (3) эскалировать индивидуальных adversarial-голосов только туда, где они несут нагрузку (refuted / central). Цифры (замерено на одной теме, гейт PASS во всех вариантах): legacy-движок 3.04M токенов / 99 агентов → sources+batch ~2.0M / 66 → линейный путь 397k свежих (30k output) / 1 агент = ×7.7 к legacy. Целевые 50–120k не добиты: 98 tool-раундтрипов пере-кэшируют контекст (cache-read дёшев, но не бесплатен) + трения первого прогона (encoding, shell). Скрипты, вынесшие механику: collect (fetch/кэш/окна/кандидаты), ledger (схема + quote-pre-gate + детерминированный verdict), prior (дедуп прогонов).

Невидимые байты ломают substring-гейт (e2e линейного пути)

bun.com/docs вставляет U+200B перед заголовками: в терминале кандидат выглядит идентично кэшу, а substring-чек падает. Фикс: zero-width (U+200B..200D, FEFF) вырезаются и в norm() гейта, и в toText() collect-а — кандидаты не могут унести невидимый байт. Урок: «дословно» определяется после нормализации, и обе стороны сравнения обязаны нормализоваться ОДНИМ кодом (поэтому norm живёт в lib/, а не копипастой).

Verbatim-по-построению бьёт verbatim-по-инструкции

Просить модель «копируй дословно» — работает до первого пересказа; давать ей выбирать предложения-кандидаты ПО ID (collect.mjs --candidates) — парафраз становится физически невозможен, и это ещё и дешевле (читаются только кандидаты). Тот же принцип, что и с гейтом: не правило, а механика, которую нельзя нарушить.

Resume-кэш Workflow — это ПРЕФИКС, и это меняет порядок фиксов

Кэшируется самая длинная неизменная ПОСЛЕДОВАТЕЛЬНОСТЬ agent()-вызовов: правка политики эскалации до снятия результата порвала бы префикс и перезапустила оплаченные голоса. Порядок: сначала фикс бага ПОСЛЕ последнего agent()-вызова (return/stats) → resume бесплатно снимает результат → потом менять политику стадий. И баг «scope is not defined» в финальном stats стоил прогона: не-agent код воркфлоу надо прогонять через node --check с заглушками ДО запуска, финальный return — самое дорогое место для ReferenceError.

Чужой скил можно читать, не исполняя (MCP-раунд)

Автоустановку выбранного мной скила (npx skills add -y) завернул классификатор — справедливо: агент сам выбрал внешний код и сам же его подтверждает. Рабочий обход без обхода запрета: скачать SKILL.md через raw.githubusercontent и следовать ЕГО инструкциям, не исполняя его код. Гайд дал правильные решения: официальный @modelcontextprotocol/sdk (не самопал JSON-RPC и не бета @modelcontextprotocol/server), zod-схемы на входах, structuredContent в ответах, тест через MCP Inspector CLI. npm-зависимости изолированы в mcp/ — zero-dep ядра не тронут. Протокольный смоук: tools/list + живой tools/call research_factcheck (фейк CVE → DISPROVEN по живому NVD) через инспектор.

Гейт для режима без «реестра» = структура, которую нельзя опустить

У fresh нет NVD, чтобы 404-нуть фейковый тренд. Работает та же идея с другой стороны: обязать строку нести platform + posted-дату в окне + URL (омиссию ловит машина), окно проверять арифметикой, цитаты — сэмплом quote-in-page на фетчабельных страницах, а JS-walled источники честно помечать unverifiable. Контекст старше окна — в таблицу БЕЗ platform-заголовка, чтобы гейт её не считал пульсом. Сид-правила (27–29) помечены design-time — им ещё предстоит заслужить место бенчмарком.

Сэмплинг-дыра: гейт, который проверяет только первые N строк, не гейт

Панель fusion поймала: verify.mjs брал первые 3/4/6 checkable-строк по порядку таблицы — остальной леджер не перепроверялся НИКОГДА. Модель ставит честные цитаты первыми, фейк — в строку 5, и отчёт проходит. Фикс: полная проверка ВСЕХ строк, cache-first (бесплатный substring по same-day кэшу collect.mjs) + живой ре-фетч по уникальным URL с бюджетом --max-live; строки сверх бюджета — явный unchecked, не молчаливый PASS. Урок: «проверили образец» ≠ «проверили»; сэмплинг в гейте — это дыра ровно там, где её не видно.

Fail-open хуже отсутствия гейта: checked===0 не может печатать PASS

checked===0 (все источники JS-стена/404) давал warning + exit 0 + штамп PASS — отчёт, цитирующий только нефетчабельные страницы, проходил. Фикс: отдельный вердикт UNVERIFIED с собственным exit 3 (0=PASS · 1=FAIL · 2=usage · 3=UNVERIFIED); слово PASS в выводе и штампе UNVERIFIED не встречается, чтобы CI-греп по «PASS» не принял его за успех (red-team). Урок: у гейта три исхода, не два — «прошёл», «провалился» и «не смог проверить»; их нельзя схлопывать, иначе неизвестность маскируется под успех.

Подделка корроборации: буст доверия нельзя брать от непроверенной строки

ledger.mjs verdict засчитывал корроборацию от ЛЮБОЙ строки с другим хостом и Jaccard≥0.5 — саму корроборирующую строку никто не проверял, и verified=yes можно было вписать в md-таблицу руками. Фиксы: (1) корроборация — только от строк с precheck=supported (сначала прогнать checkRow, потом считать буст); (2) provenance-сайдкар — verify.mjs матчит каждую verified=yes строку отчёта к строке L.json по source_url + quote_hash (sha256 от нормализованной цитаты, не сырой body-hash — red-team), нет совпадения → FAIL; (3) overlap-floor — нулевое пересечение claim↔quote роняет confidence в low. Урок: детерминированный гейт нельзя уболтать только если КАЖДЫЙ вход в доверие сам проверен.