# Ограничения: что именно ломается и как это снять

Пять ограничений, которые чаще всего мешают на реальных сайтах. Для каждого: наблюдаемое
поведение, причина (со ссылкой на код), что нужно изменить и сколько это стоит.

| # | Ограничение | Статус | Что осталось |
|---|---|---|---|
| 1 | iframe не обходится | **снято для сети и действий, частично для снимка** | same-origin обходится целиком; у кросс-доменных (OOPIF) снимается сеть (пометка `frame` в записи и в `api.md`), работают `site_snapshot` (элементы с префиксом `iframe >>> …` и координатами страницы), `site_text`, `click`, `fill`, `select`; остаются неохваченными вложенные OOPIF глубже одного уровня и `upload` внутрь фрейма |
| 2 | shadow DOM частично | **частично снято** | открытые корни: селекторы-цепочки и поиск работают; закрытые корни не видны |
| 3 | тело ответа ≤ 400 КБ | **снято** | лимит 5 МБ в воркере, тела > 200 КБ выносятся в `bodies/<id>.json` |
| 4 | один таб на сессию | **снято** | `Map<tabId, session>`, `tabId` в командах, переключатель в попапе |
| 5 | Chrome отказывает в `chrome.debugger.attach` вкладке или вешает вызов (`Cannot access a chrome-extension:// URL of different extension`) | открыто, но зависит от профиля | воспроизведено у пользователя на `mail.google.com` (отказ) и на странице поиска Google (зависание), при этом `tabs.get` вкладку отдаёт, а `chrome.debugger.getTargets` показывает обычный `page` с https-адресом. В чистом профиле с одним нашим расширением те же адреса (`accounts.google.com`, `google.com/sorry`) подключаются без ошибок — значит на вкладке есть фрейм другого расширения либо отладку перехватывает стороннее ПО. Лечится профилем без посторонних расширений |

Ниже — исходный разбор каждого ограничения; для снятых пунктов оставлено описание того, что
именно было изменено и какие границы остались.

---

## 1. iframe не обходится

**Что видно на практике.** Плашка входа, встроенная форма оплаты, виджет чата или карта живут
в `<iframe>`. Снимок их не содержит: в `dom.json` есть только `counts.frames` — число фреймов,
но ни одного узла изнутри. `site_act {op:"click", selector:"#submit"}` по кнопке внутри iframe
падает с «element not found»: поиск идёт по `document` верхнего документа, а у iframe свой
`document`.

**Причина.** Снимок и действия выполняются одним `Runtime.evaluate` в главном фрейме
(`snapshotInPage` в `sw.js`), а `document.querySelector` по спецификации не заглядывает в
документы фреймов. Сеть при этом ловится: CDP Network работает на уровне таба и видит запросы
всех фреймов — но фильтр «свой хост / JSON» отбрасывает кросс-сайтовые виджеты.

**Как снять.**

1. `Page.getFrameTree` → список `frameId`. Перехватить `Runtime.executionContextCreated`
   и запомнить соответствие `frameId → contextId`.
2. Для каждого фрейма выполнить тот же `snapshotInPage` в его контексте
   (`Runtime.evaluate` + `contextId`), в результат добавить `frameId`, `frameUrl`,
   позицию фрейма в родителе. Узлы мёржатся в общее дерево с полем `frame`.
3. Действия: резолвить элемент в контексте нужного фрейма (`Runtime.evaluate` там же),
   а координаты клика считать как `iframe.rect + element.rect` — `Input.dispatchMouseEvent`
   работает в координатах окна.
4. Селекторы: хранить как `{ frame: "<iframe src-фрагмент>", selector: "#submit" }` или
   строкой `iframe[src*="checkout"] >> #submit`; в `selectors.md` печатать эту форму.
5. Кросс-доменные iframe (OOPIF) — отдельные таргеты: `Target.setAutoAttach` с `flatten:true`
   и работа через `sessionId`. Это основной источник сложности.

**Что останется.** Фреймы с `sandbox` без `allow-scripts` и страницы с жёстким CSP:
внутрь не пролезть ни снимком, ни инъекцией. Сеть у них видна.

---

## 2. shadow DOM частично

**Что видно на практике.** Часть интерфейса (обычно дизайн-система или виджет) живёт в
`shadowRoot`. Узлы попадают в `dom.json`, но у них неверный `sel.best`: он посчитан через
`document.querySelectorAll(...)`, а такой селектор не проходит сквозь границу shadow-дерева —
получается 0 совпадений, и в качестве «устойчивого» выдаётся длинный путь, который не работает.
`click` по нему не находит элемент. Закрытые корни (`mode: "closed"`) не видны вообще.

**Причина.** Обход открытых корней есть (`if (el.shadowRoot)` в `sw.js:761`), но построение
селекторов и разрешение элементов — документные. Плюс сама модель CSS-селекторов не умеет
«проваливаться» в shadow-дерево.

**Как снять.**

1. **Селекторы-цепочки.** Для узла внутри shadow root хранить путь как массив шагов:
   `[{host: "my-widget"}, {root: "shadow"}, {selector: "button.add"}]`. Резолв — скриптом:
   `document.querySelector(host).shadowRoot.querySelector(sel)`. В `selectors.md` печатать
   читаемую форму `my-widget >> button.add`.
2. **Действия.** Не использовать `document.querySelector` вообще: всегда резолвить через
   функцию-обходчик, которая идёт по цепочке и возвращает `rect`; клик — по координатам
   (этот путь уже отлажен и работает в фоновой вкладке).
3. **Закрытые корни.** Патч `Element.prototype.attachShadow` в MAIN-мире через
   `Page.addScriptToEvaluateOnNewDocument`: сохранять созданные корни в массив на `window`,
   затем обращаться к ним из `Runtime.evaluate`. Работает, пока страница не защищается от
   переопределения прототипа — на банковских и антибот-страницах это первое, что ломают.
4. **Проверка на каждом шаге:** после снятия снимка прогонять `sel.matches > 0` для всех
   кандидатов и помечать нерабочие как `unresolved`, а не выдавать их как «устойчивые».

**Что останется.** Closed roots на страницах, которые патчат прототипы и используют
`Trusted Types`: останется только сеть и координатные клики.

---

## 3. Тело ответа ≤ 400 КБ

**Что видно на практике.** Ответ на 2 МБ (список товаров, JSON с картинками, экспорт) приходит
обрезанным: в `network.jsonl` лежат первые 400 000 символов, остальное потеряно без пометки
внутри тела. Очень большие ответы могут вообще не сняться: CDP-буфер ограничен
`maxResourceBufferSize: 16 МБ` и `maxTotalBufferSize: 64 МБ` (`sw.js:184-186`), после чего
`Network.getResponseBody` отвечает ошибкой «No resource with given identifier».
В артефактах поверх этого: `dom.html` ≤ 2 МБ (`artifacts.js:11`), а в `api.md` тела печатаются
до 200 КБ (`artifacts.js:12`).

**Причина.** Три независимых потолка, каждый со своей целью: память service worker'а,
размер HTML-файла, читаемость markdown-отчёта.

**Как снять.**

1. **Сделать лимиты настройками**, а не константами: `maxBodyChars`, `maxHtmlBytes`,
   `markdownBodyChars` в `cordis.patch.yml` профиля — тогда под конкретный сайт поднимается
   нужный потолок без правки кода.
2. **Крупные тела — в отдельные файлы.** Вместо обрезки: `bodies/<id>.json` рядом с
   `network.jsonl`, а в самом jsonl — ссылка `{"bodyRef": "bodies/12.json", "bodyBytes": 2097152}`.
   Файл читается по требованию, контекст модели не раздувается.
3. **Base64 (бинарные ответы)** сейчас не сохраняются вообще. Писать их как
   `bodies/<id>.bin` и отдавать путь — картинки и PDF иногда нужны для написания расширения.
4. **Поднять CDP-буферы** под сценарий (например, 256 МБ) и следить за памятью воркера:
   у Chrome это реальная память вкладки, а не диск.
5. **Проверка:** страница с ответом 1–2 МБ, в пакете тело должно быть целиком и с корректной
   длиной; в `api.md` — пометка «тело в файле X».

---

## 4. Один таб на сессию

**Что видно на практике.** Пока подключена одна вкладка, вторая недоступна: `site_tabs
{action:"claim"}` отключает предыдущую, а `site_status` показывает единственную цель. Нельзя
держать запись API на одной странице, а кликать на другой, и нельзя сверить два состояния
одного сайта в двух вкладках.

**Причина.** В service worker одна переменная `state.attachedTabId` (`sw.js:23`), один
`Network.enable` и один общий буфер записей; в мосте — единственное поле `target`.

**Как снять.**

1. **В расширении:** `Map<tabId, {attached, target, recording, entries}>`. `chrome.debugger`
   допускает несколько сессий одновременно — по одной на таб.
2. **Протокол:** добавить `tabId` в команды и результаты; события сети помечать `tabId`;
   в мосте — массив целей и `activeTabId` как значение по умолчанию.
3. **Инструменты:** необязательный параметр `tab` у всех `site_*` (по умолчанию — активная),
   `site_tabs {action:"use", tabId}`, а в `site_status` — список подключённых.
4. **Popup:** строки со списком подключённых вкладок, галочка «записывать» у каждой.
5. **Аккуратность в артефактах:** пакет привязан к хосту и моменту — при двух вкладках одного
   сайта нужно явно указывать `tabId` в `meta.json`, иначе пакеты перепутаются.

**Риски.** Больше памяти и одновременных сессий отладки; сложнее объяснять модели, куда идёт
команда. Поэтому дефолт «активная вкладка» стоит сохранить, а мультитаб включать флагом.

---

## Порядок, в котором это выгодно делать

1. **Тело ≤ 400 КБ** — самое дешёвое: конфиг + вынос тел в файлы, полдня.
2. **Shadow DOM (открытые корни)** — селекторы-цепочки и резолв через обходчик, полдня-день;
   сразу убирает ложные «устойчивые» селекторы.
3. **iframe** — самая большая функциональная дыра (формы, оплата, виджеты), но и самая дорогая
   из-за OOPIF.
4. **Мультитаб** — удобно, но чаще всего можно обойтись повторным `claim`; делать последним.
