From 71edb9a36c0b04218d2df8281c04dff599468667 Mon Sep 17 00:00:00 2001 From: Socialpranker <273312799+Socialpranker@users.noreply.github.com> Date: Sun, 6 Sep 2026 07:41:04 +0200 Subject: [PATCH] =?UTF-8?q?feat:=20=D1=81=D0=B1=D0=BE=D1=80=20=D0=B8=D1=81?= =?UTF-8?q?=D1=82=D0=BE=D1=87=D0=BD=D0=B8=D0=BA=D0=BE=D0=B2=20=E2=80=94=20?= =?UTF-8?q?=D0=BB=D0=B5=D1=81=D1=82=D0=BD=D0=B8=D1=86=D0=B0=20fetch=20?= =?UTF-8?q?=D1=88=D1=82=D0=B0=D1=82=D0=BD=D0=BE,=20=D0=B3=D0=B5=D0=B9?= =?UTF-8?q?=D1=82=20=D1=82=D0=BE=D0=BD=D0=BA=D0=B8=D1=85=20=D0=B8=D1=81?= =?UTF-8?q?=D1=82=D0=BE=D1=87=D0=BD=D0=B8=D0=BA=D0=BE=D0=B2,=20=D0=B2?= =?UTF-8?q?=D1=82=D0=BE=D1=80=D0=BE=D0=B9=20=D0=BF=D0=BE=D0=B8=D1=81=D0=BA?= =?UTF-8?q?=D0=BE=D0=B2=D1=8B=D0=B9=20=D0=B4=D0=B2=D0=B8=D0=B6=D0=BE=D0=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - fetch_source.py: robots.txt fail-closed по RFC 9309, content_sha256 и снапшот страницы в frontmatter - validate_phases: тонкий источник (<2 цитат или access PARTIAL/closed) не плечо триангуляции, ошибка на medium+ - subagents_v2: WebFetch дал тонко ⇒ сразу fetch_source.py, не по желанию - search_query.py: Brave/Tavily/Exa как вторая ось поиска, ключи аудируются в фазе 3.5 --- SKILL.md | 5 +- references/api_sources/search/brave_search.md | 8 +- references/api_sources/search/exa.md | 4 + references/api_sources/search/tavily.md | 4 + references/capability_discovery.md | 10 +- references/fetch_ladder.md | 4 +- references/source_dispatch.md | 2 + references/source_scoring.md | 6 + references/subagents_v2.md | 39 +++- runner/capabilities.py | 14 +- scripts/README.md | 15 ++ scripts/fetch_source.py | 85 ++++++-- scripts/search_query.py | 205 ++++++++++++++++++ scripts/validate_phases.py | 115 ++++++++++ tests/test_capabilities.py | 6 +- tests/test_docs_swarm.py | 22 +- tests/test_fetch_source.py | 178 ++++++++++++++- tests/test_search_query.py | 133 ++++++++++++ tests/test_validate_phases.py | 135 +++++++++++- 19 files changed, 946 insertions(+), 44 deletions(-) create mode 100644 scripts/search_query.py create mode 100644 tests/test_search_query.py diff --git a/SKILL.md b/SKILL.md index 5b63f91..dc4fe58 100644 --- a/SKILL.md +++ b/SKILL.md @@ -42,7 +42,7 @@ description: "Meta-research под вопрос или решение: веб-п 3. **Plan** [`opus`/medium] — `plan.md` по шаблону §0–16 из `workflow.md`: HEADER → SCOPE → STRUCTURE → EXECUTION → TRACKING. Несущее: acceptance criteria, гипотезы, risk register, subtopic↔blocks mapping (least-to-most для многошаговых вопросов), sourcing strategy §12, opposition queries, stop-criteria. 3.5. **Capability Discovery** [`sonnet`/low] (deep — обязательна) — audit env vars, подтемы → доступные API, fallback на awesome-lists. См. `capability_discovery.md`. 3.7. **Plan-review gate** [`sonnet`/low] (shallow — skip) — единственная human-in-the-loop точка ПЕРЕД дорогой Фазой 4: показать сжатый план (вопрос, решение, жанр, гипотезы, каналы, стоп-критерий, routing). **deep — ЖДАТЬ явного «Ок»; medium — soft.** Плюс **скаут-пасс** (deep — рекомендуется): 3–4 `Explore` на `haiku` ищут непокрытые подвопросы, а не источники; выход — правки `plan.md`, ноль записей в `sources/`. См. `plan_gate.md`. -4. **Поиск** [main `sonnet`/medium; sub-agents: `haiku` web/api, `sonnet` academic/long-source] — (4.0) Source Dispatch по матрице → `plan.md` §12; количественный подвопрос ⇒ primary-канал registry/API. (4.1) medium/deep — `general-purpose` суб-агенты параллельно, каждому свой диапазон id (`s01-s09`, `s10-s19`…) и своя ось поиска, не только подтема; shallow — главный поток. (4.2) Fetch, дедуп с замером `overlap_rate`. (4.3) Агент сам пишет `sources/NN_slug.md`, в главный поток — только index-строки. После раунда 1 — snowball. Loop: goal-check → bounded deviation → circuit breaker (2 раунда без нового ⇒ стоп, остаток в Open Questions). **Окно раунда пересобирается, не накапливается** (medium/deep): `state.md` ПЕРЕЗАПИСЫВАЕТСЯ перед каждым раундом (`## Known` статусами со ссылками · `## Gaps` · `## Next`, ≤6 КБ), планирование по нему, а не по транскрипту. См. `source_dispatch.md`, `subagents_v2.md`. +4. **Поиск** [main `sonnet`/medium; sub-agents: `haiku` web/api, `sonnet` academic/long-source] — (4.0) Source Dispatch по матрице → `plan.md` §12; количественный подвопрос ⇒ primary-канал registry/API. (4.1) medium/deep — `general-purpose` суб-агенты параллельно, каждому свой диапазон id (`s01-s09`, `s10-s19`…) и своя ось поиска, не только подтема; shallow — главный поток. (4.2) Fetch, дедуп с замером `overlap_rate`; WebFetch вернул тонко (<500 знаков, нет цитаты, блок/paywall) ⇒ сразу `fetch_source.py`, не по желанию. (4.3) Агент сам пишет `sources/NN_slug.md`, в главный поток — только index-строки. После раунда 1 — snowball. Loop: goal-check → bounded deviation → circuit breaker (2 раунда без нового ⇒ стоп, остаток в Open Questions). **Окно раунда пересобирается, не накапливается** (medium/deep): `state.md` ПЕРЕЗАПИСЫВАЕТСЯ перед каждым раундом (`## Known` статусами со ссылками · `## Gaps` · `## Next`, ≤6 КБ), планирование по нему, а не по транскрипту. См. `source_dispatch.md`, `subagents_v2.md`. 5. **Claims-ledger + триангуляция** [`haiku`/low] — `claims.csv` (схема колонок — `source_scoring.md`). `triangulated` ⟺ ≥3 источника И ≥2 типа И ≥2 корня (`root:`) И ≥2 пути (`discovery_path:`); иначе `single-type`/`single-root`/`single-path`, потолок medium. Без primary — потолок medium; caveat (`vendor`/`self-reported`/`disputed:sNN`) — потолок medium, `disputed` без арбитра → low. **Защита меньшинства:** непогашенный `dissent` от `Primary`/`credibility ≥ 4` ⇒ `contested` независимо от большинства, обе позиции в отчёт. Gap-волна на не-triangulated, max 2 круга, иначе `data-insufficient`. См. `source_scoring.md`. 5.5. **Evidence-фильтр: relevance × authority** [`sonnet`/low] (medium/deep — обязательно) — фильтр на ВХОДЕ синтеза. **Relevance:** пара (claim, source) → Correct/Ambiguous/Incorrect по дословным цитатам → relevant-only цитаты в `evidence/CN.md`; claim без relevant-источника → `data-insufficient` или до-поиск. **Authority** (несущие пары: claim в memo/F1/F9, ИЛИ с числом, ИЛИ источник единственный корень, ИЛИ `caveat` ≠ `-`): «вправе ли ЭТОТ источник утверждать ЭТО» → `qualified`/`unqualified-for-this-claim`/`unknown` → `.verify/authority.json`. **`unknown` — карантин:** не единственная опора, не `high`. См. `evidence_filter.md`. 5.7. **Сверка с вики** [`sonnet`/low] (medium/deep — обязательно) — `claims.csv` прогона против кросс-прогонной вики, **до синтеза**: расхождение с прошлым ресёрчем обязано попасть в отчёт, заметить его в Фазе 7 значит заметить поздно. `wiki_pair.py build` собирает пары и сам закрывает всё, что не требует суждения; остаток идёт в `.verify/wiki_pairs.json` → вердикт из четырёх (`same-claim-agree`/`same-claim-conflict`/`different-claim`/`unknown`) → `wiki_pair.py record`. **`unknown` — карантин**, не конфликт. Подтверждённый конфликт = обе позиции в отчёт, потолок `medium`, без арбитра `contested`. Срез по `MAX_ADJUDICATED` называть вслух. Пороги прескрина и правила `record` — `wiki.md`. @@ -125,6 +125,7 @@ description: "Meta-research под вопрос или решение: веб-п - Фаза 6.5: не доверять наличию ссылки — проверять entailment по дословной цитате; вердикты писать в `.verify/*.json` и не пересчитывать в rubric/F10; пары брать из `evidence/`, не пересканировать `sources/`. Чинить отчёт, а не ledger. - Для fetch+save и red team — `general-purpose` с явным диапазоном номеров, не `Explore` (read-only, только разведка). Не запускать суб-агентов последовательно — только параллельно в одном сообщении. - Не сжимать `sources/` в один файл, не выводить результат только в чат. Не гонять шаги finish-up поодиночке вместо `finish.py` — пропуск флага (`--out`, `--strict`) и есть пропуск фазы. +- Тонкий источник (<2 дословных цитат или `access:` PARTIAL/closed) не считается плечом триангуляции — перефетчить лестницей или понизить статус тезиса. - Не обходить WebFetch произвольным `bash`/`curl`. Единственный санкционированный fallback — `scripts/fetch_source.py` (Фаза 4.2): он читает robots.txt, санитайзит страницу от prompt injection и проставляет `fetch_tier`. Вывод ручного `curl` в `sources/` не кладётся. - Не принимать `fetch_source.py` за средство против paywall и анти-бот-защиты: `auth-wall` и `antibot` для него — терминальный вердикт. Дальше — fallback-протокол `channels.md` или endpoint из `api_sources/`. - Не рисовать в отчёте число, которого нет в `numbers.csv`, и не подбирать палитру фигур на глаз — она валидируется скриптом. @@ -144,7 +145,7 @@ Update тоже идёт в вики: `wiki_ingest.py` после дельты ( **Условные — грузить, когда прогон дошёл до условия, а не заранее:** `capability_discovery.md` и `awesome_lists_registry.md` — Фаза 3.5 (обязательна только на deep) · `stat_sources/INDEX.md` (33 категории) и `api_sources/INDEX.md` (47+ endpoints) — Фаза 4, когда подвопрос количественный или Source Dispatch ведёт в registry/API · `refresh_protocol.md` — только режим `update`. -**По фазам:** `subagents_v2.md` (4) · `fetch_source.py` + `channels.md` §«Bot-block ≠ paywall» (4.2, только когда WebFetch ответил «unable to fetch from …») · `source_scoring.md` (шкалы, provenance, claims-ledger, dissent, `numbers.csv` — 5–6) · `evidence_filter.md` (5.5) · `synthesis_outline.md` (6) · `adversarial_pass.md` (6) · `runtime_verification.md` (6.5) · `report_export.md` (6.9) · `swarm_postprocess.md` (после 7) · `decision_walkthrough.md` (8). +**По фазам:** `subagents_v2.md` (4) · `fetch_source.py` + `channels.md` §«Bot-block ≠ paywall» (4.2, штатно при «unable to fetch» И при тонком ответе) · `source_scoring.md` (шкалы, provenance, claims-ledger, dissent, `numbers.csv` — 5–6) · `evidence_filter.md` (5.5) · `synthesis_outline.md` (6) · `adversarial_pass.md` (6) · `runtime_verification.md` (6.5) · `report_export.md` (6.9) · `swarm_postprocess.md` (после 7) · `decision_walkthrough.md` (8). **Блоки (по выбранному жанру):** `frame.md` F1-F10 · `explain.md` E1-E14 · `compare.md` C1-C13 · `map.md` M1-M12 · `validate.md` V1-V10 · `analyze.md` A1-A13 · `close.md` Z1-Z12 · `people.md` P1-P7 · `numbers.md` N1-N8 · `context.md` X1-X7. diff --git a/references/api_sources/search/brave_search.md b/references/api_sources/search/brave_search.md index ac83f40..676d899 100644 --- a/references/api_sources/search/brave_search.md +++ b/references/api_sources/search/brave_search.md @@ -47,15 +47,19 @@ JSON с organic results, news, videos, infobox. 1. https://api.search.brave.com → sign up 2. Получи API key -3. В env: `export BRAVE_API_KEY="BSA..."` +3. В env: `export BRAVE_SEARCH_API_KEY="BSA..."` ## Query patterns +Прямой вызов из скилла: `python3 scripts/search_query.py --engine brave --query "..." [--n 10] [--json]` +(читает `BRAVE_SEARCH_API_KEY` из env, exit 2 если ключа нет). Ниже — сырой HTTP-контракт, +который скрипт реализует. + ### Web search ``` GET https://api.search.brave.com/res/v1/web/search?q={query}&count=20 -Headers: X-Subscription-Token: {BRAVE_API_KEY} +Headers: X-Subscription-Token: {BRAVE_SEARCH_API_KEY} ``` ### News search diff --git a/references/api_sources/search/exa.md b/references/api_sources/search/exa.md index 65ca339..04d6d98 100644 --- a/references/api_sources/search/exa.md +++ b/references/api_sources/search/exa.md @@ -50,6 +50,10 @@ JSON с результатами ranked by semantic similarity to query. ## Query patterns +Прямой вызов из скилла: `python3 scripts/search_query.py --engine exa --query "..." [--n 10] [--json]` +(читает `EXA_API_KEY` из env, exit 2 если ключа нет). Ниже — сырой HTTP-контракт, +который скрипт реализует. + ### Neural search ``` diff --git a/references/api_sources/search/tavily.md b/references/api_sources/search/tavily.md index 7cf797e..a5a0c6c 100644 --- a/references/api_sources/search/tavily.md +++ b/references/api_sources/search/tavily.md @@ -48,6 +48,10 @@ JSON с готовыми **answers + sources**. Tavily — это search API, с ## Query patterns +Прямой вызов из скилла: `python3 scripts/search_query.py --engine tavily --query "..." [--n 10] [--json]` +(читает `TAVILY_API_KEY` из env, exit 2 если ключа нет). Ниже — сырой HTTP-контракт, +который скрипт реализует (basic search, без `include_answer`/`include_domains`). + ### Basic search ``` diff --git a/references/capability_discovery.md b/references/capability_discovery.md index 1aa072b..9f4c5cf 100644 --- a/references/capability_discovery.md +++ b/references/capability_discovery.md @@ -15,6 +15,8 @@ Capability Discovery закрывает этот gap. **Прозрачность для пользователя:** что используется, что пропускается, что бы стоило настроить. +**Второй поисковый движок.** Один движок = одна траектория поиска — конформизм, который правило триангуляции запрещает для источников и должно запрещать для самого канала поиска. Если в env есть `BRAVE_SEARCH_API_KEY`, `TAVILY_API_KEY` или `EXA_API_KEY` — на medium/deep Phase 4 обязана выделить одну поисковую ось под него через `scripts/search_query.py --engine {brave,tavily,exa} --query "..."`, параллельно обычному WebSearch харнесса. Пересечение результатов между движками фиксируется как `overlap_rate` в `plan.md` §15 (низкий overlap — сигнал, что второй движок реально расширяет покрытие, а не дублирует первый). + ## Workflow фазы ### Step 1: Audit env vars @@ -34,10 +36,10 @@ if env $GITHUB_TOKEN exists → mark GitHub as 'authenticated' |---|---|---| | `FRED_API_KEY` | FRED economic data | optional (HTML fallback есть) | | `GITHUB_TOKEN` | GitHub Search | optional (60/h без, 5000/h с) | -| `BRAVE_API_KEY` | Brave Search | **не используется** — скилл ходит через WebSearch харнесса | -| `TAVILY_API_KEY` | Tavily | **не используется** — оставлено для внешних раннеров | -| `EXA_API_KEY` | Exa.ai | **не используется** — оставлено для внешних раннеров | -| `SERPAPI_KEY` | SerpAPI | **не используется** — оставлено для внешних раннеров | +| `BRAVE_SEARCH_API_KEY` | Brave Search | второй поисковый движок, обязателен для одной оси Phase 4 если ключ есть | +| `TAVILY_API_KEY` | Tavily | второй поисковый движок, обязателен для одной оси Phase 4 если ключ есть | +| `EXA_API_KEY` | Exa.ai | второй поисковый движок, обязателен для одной оси Phase 4 если ключ есть | +| `SERPAPI_KEY` | SerpAPI | **не используется** — платный, без free tier | | `NEWSAPI_KEY` | NewsAPI | для news | | `ALPHA_VANTAGE_KEY` | Alpha Vantage | для stock prices | | `CRUNCHBASE_API_KEY` | Crunchbase | для company data | diff --git a/references/fetch_ladder.md b/references/fetch_ladder.md index 2fc4318..0c48bbf 100644 --- a/references/fetch_ladder.md +++ b/references/fetch_ladder.md @@ -22,6 +22,8 @@ uv run scripts/fetch_source.py -o /.fetch/NN.md --meta /.fetch/N - **Фид не заменяет статью.** Из него берутся заголовок, дата, аннотация и URL — но не полный текст. Цитату из аннотации помечать как таковую, не выдавать за цитату из статьи. - **PDF не markdown-ится, а сохраняется как есть.** Скрипт определяет тип по `Content-Type` и магии `%PDF-`, кладёт файл с расширением `.pdf` и ставит `content_kind: pdf` в источник. Дальше — штатный `Read` с диапазоном `pages`; он читает PDF нативно. Для академического канала это основной путь: препринты и рабочие бумаги приезжают PDF-ами, а не страницами. Фид так же сохраняется дословно в `.xml` — markdownify уничтожает структуру, ради которой фид и брали. - **Exit 3 — стоп по умолчанию.** Это `Disallow` для всех клиентов, а не AI-специфичное исключение. Флаг `--ignore-robots` существует, но это решение пользователя: суб-агент его не ставит — пишет в `gaps` и идёт дальше. +- **robots.txt недоступен — тоже exit 3, не «разрешено».** По RFC 9309: 404/410 значит «правил нет», это `allowed`. А 5xx, таймаут или сетевая ошибка — «не смогли узнать», и это fail-closed, не fail-open. Frontmatter получает `robots: unreachable`; `--ignore-robots` снимает и этот блок так же, как обычный `Disallow`. - **Код 200 ≠ успех.** Скрипт классифицирует по паре (статус, содержимое): заглушка под 200 это `antibot`, а не источник. Не подменять эту проверку на «файл записался» — scrapling пишет файл и выходит нулём даже на 401. -- **Что уезжает в источник.** Из `--meta` в frontmatter `sources/NN.md` идут `access:`, `fetched:`, `fetch_tier:` и `fetch_note:`, если он есть. Тир — часть провенанса: страница, взятая браузером поверх AI-исключения, не равна отданной добровольно. +- **Что уезжает в источник.** Из `--meta` в frontmatter `sources/NN.md` идут `access:`, `fetched:`, `fetch_tier:`, `content_sha256:`, `snapshot:` и `fetch_note:`, если он есть. Тир — часть провенанса: страница, взятая браузером поверх AI-исключения, не равна отданной добровольно. +- **`content_sha256` и `snapshot` — на случай, если сайт молча переписал страницу.** `content_sha256` — хэш извлечённого текста; `snapshot` — путь к сырой копии тела ответа рядом с `--out` (`<имя>.snapshot.html` для HTML, `.snapshot.bin` для остального). Без `-o` или с `--no-snapshot` копии нет, `snapshot: -`. - Извлечение всегда идёт в режиме `main_content_only` — он же санитайзер prompt injection (режет скрытые и `aria-hidden` элементы, `