Skip to content
Back to skills

Gdocs

ASecurity

Вести живой документ в Google Docs, который правится итеративно и его читает заказчик: "поправь ТЗ в доке", "обнови документ у клиента". Разовая сборка - `md-docx`/`md-pdf`.

  • 8 stars
  • 0 votes
  • 0 copies
  • 0 views
  • Added September 29, 2026
developmentpythongobashapi

Works with

  • cli
  • api
  • mcp

Security analysis

A100/100

Scanned September 29, 2026

npx -y skills add dewil/claude-toolkit --skill gdocs --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Gdocs?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for Gdocs
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/dewil-gdocs/badge)](https://www.skillsdirectory.com/skills/dewil-gdocs)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
---
name: gdocs
description: Вести живой документ в Google Docs, который правится итеративно и его читает заказчик: "поправь ТЗ в доке", "обнови документ у клиента". Разовая сборка - `md-docx`/`md-pdf`.
---

# gdocs

Ведение живого документа в Google Docs: `scripts/gdocs.py` с четырьмя подкомандами. Только stdlib, авторизация общая с `gsheets.py`.

## Зачем это поверх прямых вызовов API

Документ, который правится программно и его же читает заказчик, копит класс дефектов, **невидимых для API**: все вызовы возвращают успех, а поломку видит только человек, открывший документ (`rules/silent-failure.md`). Ни одна из граблей ниже не следует из документации Docs API - каждая стоила отдельного разбора на живом документе.

Пробел в каноне закрывается ровно этот: работа с Sheets есть (`scripts/gsheets.py`), сборка документа из markdown есть (`md-docx`, `md-pdf`), а ведения живого документа не было.

## Когда применять

- Документ ведется **итеративно**: правки идут сериями, между ними его читают и правят люди.
- Документ имеет **структуру с номерами** - разделы, подпункты, перекрестные ссылки "см. 10.7".
- Документ **уходит наружу**: заказчику, на согласование, в приложение к договору.

## Когда НЕ применять

- **Разовая генерация документа** из готового текста - это `md-docx` (или `md-pdf`): собрал и отдал, вести нечего.
- **Документ без нумерации и структуры** - заметка, черновик, произвольный текст: `normalize` и `check` тут проверяют то, чего нет, а `blanks` навяжет чужое правило.
- **Таблица данных** - это Sheets (`scripts/gsheets.py`, `rules/google-sheets-mcp.md`), не Docs.
- **Чужой документ, который мы только читаем** - хватит `snapshot`; `normalize` и `blanks` чужую разметку менять не должны.

## Грабли Docs API

**1. Вставленный текст наследует форматирование точки вставки.** `replaceAllText` и `insertText` своего стиля не несут: результат перенимает оформление места, куда попал. Три наблюдавшихся исхода: заменил абзац-заголовок на "заголовок + тело" - **все тело стало заголовком** и уехало в структуру документа; вставил список строк вплотную к заголовку - **каждая строка стала заголовком**; заменил текст внутри бывшего заголовка - абзац остался обычным, но **сохранил кегль 16pt вместо 11**. Снаружи API все три - успешный ответ.

Лечится не аккуратностью, а проходом-нормализатором после каждой правки. Инвариант проверяемый: заголовок - это строка вида `N. ЗАГЛАВНЫМИ` или `N.M. ЗАГЛАВНЫМИ`, все остальное - обычный текст без явного `fontSize`. **Опознается по тексту, а не по разметке** - разметка и есть то, что врет. Ловится в обе стороны: и текст, ставший заголовком, и заголовок, потерявший разметку.

**2. Удаления и вставки в одном `batchUpdate` ломают индексы.** Операции применяются последовательно, и индексы, посчитанные по исходному документу, после первого же удаления смещаются; сортировка по убыванию спасает только внутри однородного набора. Наблюдалось: смешанный батч разорвал заголовок пополам - `5.9. О КОМПАНИИ` превратился в `5.9. О` плюс отдельный абзац `КОМПАНИИ`. **Удаления и вставки - разными запросами, между ними документ перечитывается.** Дороже на один запрос, зато не портит документ.

**3. Оглавление API вставлять не умеет.** Запроса нет. Ручной список разделов текстом - плохая замена: он не обновляется и, вставленный рядом с заголовком, сам становится набором заголовков (грабля 1). Правильно: разметить заголовки стилями и сказать пользователю, что оглавление вставляется штатно (Вставка - Оглавление) и дальше собирается само.

**4. Нативные списки несовместимы с перекрестными ссылками.** В нативном списке Docs номер живет в разметке, а не в тексте: сослаться на него нельзя, а при вставке пункта все ссылки "см. 10.7" начинают врать. **В документе с перекрестными ссылками номера остаются частью текста.** Цена - ручная перенумерация, она же дает контроль. Визуальную вложенность дают отступы абзаца, а не списки.

**5. Удаление пункта оставляет три следа**, и проверять надо все три сплошным проходом: осиротевшую пустую строку, дыру в нумерации (`10.6` -> `10.8`) и перекрестные ссылки в никуда.

**6. Правило пустых строк зависит от типа раздела**, единого нет - и в этом ловушка: приведешь весь документ к одному виду, испортишь половину. Раздел, где подпункты это **заголовки блоков с телом**, - перед каждым ровно одна пустая строка. Раздел, где подпункты это **список коротких пунктов**, - пустых строк между ними нет. Тип определяется автоматически: по тому, есть ли под подпунктом непустое тело.

**7. Документ живет только в облаке.** Его правят несколько человек, а история версий Google неудобна для сравнения. Снимок в markdown рядом с проектом дает обычный diff: видно, что изменилось между правками и что внес заказчик.

## Порядок работы

1. **Правку вносить заменой по точному фрагменту текста.** Тогда чужая правка не будет затерта молча: замена просто не найдет фрагмент, и это видно по `occurrencesChanged: 0`.

   **Проверять надо `occurrencesChanged == 1`, а не `> 0`.** Двойка означает, что фрагмент встретился дважды и вторую копию мы затерли не глядя; API при этом отвечает успехом. Отсюда же вторая ловушка - **цепочка замен, наступающая сама на себя**: если `new` одной пары окажется подстрокой `old` другой, вторая замена попадет в уже переписанный текст. Серию замен подавай так, чтобы фрагменты не пересекались, а после серии сверяй число затронутых мест с числом задуманных.

   **Удалять пункт - `deleteContentRange` по границам абзаца, с конца документа к началу**, а не заменой на пустую строку: замена оставляет пустой абзац, который потом придется искать через `blanks`, а сортировка от конца нужна по грабле 2 - индексы смещаются.
2. **После каждой содержательной правки** - `normalize` и `blanks`, а не в конце работы: дефект, найденный через десять правок, уже неотличим от исходного текста.
3. **Перед отправкой наружу** - `check` (нумерация и ссылки) и `snapshot` (копия рядом с проектом).
4. Заголовки размечать стилями сразу; оглавление текстом не вставлять.
5. **Пользователь видит в документе не то, что вернул API, - верить пользователю** и смотреть фактическое состояние документа, а не свой предыдущий ответ. Именно так были найдены грабли 1 и 2.

## CLI

```bash
python3 scripts/gdocs.py normalize <doc_id>          # предпросмотр
python3 scripts/gdocs.py normalize <doc_id> --send   # записать
python3 scripts/gdocs.py blanks <doc_id> --send
python3 scripts/gdocs.py check <doc_id>              # только отчет
python3 scripts/gdocs.py snapshot <doc_id> -o тз.md  # только чтение
```

`doc_id` - из URL документа (`.../document/d/<doc_id>/edit`).

**Дефолт у `normalize` и `blanks` - dry-run**, как у остальных пишущих скриптов канона: показывает построчно, что изменится. Документ общий, и правка видна заказчику сразу - показывай вывод пользователю и получай "ок" перед `--send`. `check` и `snapshot` не пишут никогда.

**`check` возвращает ненулевой код**, когда что-то нашел: годится как гейт перед отправкой.

## Идемпотентность и границы правки

- **Повторный прогон `normalize` и `blanks` ничего не делает** - оба приводят документ к инварианту, а не накладывают правку. Проверено на живом документе: второй прогон дает "расхождений нет".
- **`normalize` не трогает пустые абзацы**, а `blanks` не трогает стили - иначе две команды дрались бы за один абзац.
- **Смена стиля индексы не двигает**, поэтому `normalize` идет одним батчем. `blanks` двумя с перечитыванием между ними - по грабле 2.
- **Абзацы внутри таблиц видны всем командам**: иначе `normalize` пропустил бы текст в таблицах, а `check` - ссылки в них.
- **Скрипт не переносит и не удаляет содержимое** - только стили и пустые строки. Перенумерация, удаление пунктов и правка текста остаются за агентом и человеком.

## Что скрипт не умеет, и это осознанно

Проверено состязательным ревью в два раунда; перечисленное ниже - остаточные ограничения, а не недосмотр.

- **Намеренный кегль 14/16/20pt неотличим от унаследованного.** Снимается размер, одинаковый во всем абзаце и совпадающий с кеглем заголовка Docs; цитата 12pt, подпись 9pt и цена 18pt переживают нормализацию, а намеренный 16pt - нет. Документ с перенастроенными named styles (унаследованный кегль не из этого набора) `normalize` не почистит.
- **Заголовок со строчной латинской буквой внутри слова не опознается** (`ПРОЦЕСС cДАЧИ` с латинской `c`). Промах безопасный: заголовок просто останется без разметки, а не текст уедет в структуру.
- **Перед первым подпунктом сразу после заголовка раздела пустая строка не ставится** - там уже есть граница раздела. Правило "ровно одна пустая" действует между подпунктами.
- **Абзацы до первого заголовка верхнего уровня `blanks` не трогает**: без раздела непонятно, какое правило применять.
- **`check` не различает контекст.** Ссылка "см. 9.9" внутри примера кода или цитаты будет показана как битая. Это осознанно: различать контекст можно только по стилям, а стили и есть то, что врет. Команда ничего не правит, поэтому ложная тревога стоит одной строки в отчете.

  **Но доля имеет значение.** Первая редакция искала цели ссылок только среди размеченных заголовков, и на живом документе, где половина подпунктов разметку потеряла, получилось 16 ложных срабатываний из 16 - гейт красный всегда, то есть проверкой быть перестал (`rules/silent-failure.md`). Теперь цель ищется шире: заголовок **или** абзац, начинающийся с номера. Найденная не в заголовке цель отмечается в отчете отдельно - это не битая ссылка, а работа для `normalize`.

  Обратная плата у этого своя, и ее надо знать: номер, живущий только в цитате или в тексте ("2.5. миллиона"), теперь засчитается как существующая цель, и ссылка на несуществующий пункт может проскочить. Меняли шило на мыло сознательно: сплошное ложное срабатывание обесценивает гейт целиком, единичный пропуск - нет.
- **`blanks` пишет двумя запросами и не является транзакцией.** Между ними документ перечитывается, и правка привязана к ревизии; но если чужая правка придет между запросами, удаления окажутся применены, а вставки нет. Скрипт скажет об этом прямо - повторить надо весь прогон, с предпросмотра.
- **`snapshot` выгружает тело первой вкладки.** Сноски, колонтитулы и остальные вкладки не попадают; оглавление, картинки и разрывы отмечаются комментарием, а многовкладочный документ получает пометку "НЕПОЛНЫЙ СНИМОК". Молча не теряется ничего.

## Авторизация

Общая с `gsheets.py`: `~/.config/gsheets/auth.json` (`client_id`, `client_secret`, `refresh_token`, `token_uri`, права 600). Модуль подгружается сбоку - два места, работающих с ключами, разошлись бы на первом же фиксе; нет `gsheets.py` рядом - скрипт скажет, что взять из канона.

Нужна область `documents` у того же OAuth-клиента. Ее нет - Docs API отвечает `403 insufficient`, и скрипт печатает, что делать. Токен - секрет: в синкаемые папки, заметки и артефакты не класть (`rules/secrets-handling.md`).

## Связанные файлы

- `scripts/gdocs.py` в корне проекта - движок (dry-run по умолчанию).
- `scripts/gsheets.py` - сосед по авторизации, он же для таблиц.
- `rules/google-sheets-mcp.md` - почему к Google ходим скриптами, а не только MCP.
- `skills/md-docx`, `skills/md-pdf` - разовая сборка документа из markdown, другой случай.

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…

Newsletter

Get the weekly list of safe new skills, and the ones we pulled.