diplodoc-platform / diplodoc-platform/cli
Встраивание файлов из внешних источников (`{% include-code %}`)
- Dominant language
- TypeScript
- Stars
- 123
- Forks
- 51
- Avg merge
- 10h 50m
- Merged PRs (30d)
- 38
Description
## Проблема
Почти любой код в документации откуда-то скопирован: примеры, конфигурационные файлы, манифесты,
схемы, скрипты. Это не редкий случай, а норма — у фрагмента почти всегда есть оригинал, который
живёт своей жизнью и меняется независимо.
Копия расходится с оригиналом, и это расхождение **ничем не обнаруживается**: правка оригинала не
ломает сборку документации и не видна в её ревью. Устаревший фрагмент выглядит ровно так же, как
актуальный, — до тех пор, пока читатель не попробует его применить.
Расстояние до оригинала на суть не влияет. Он может лежать в соседнем каталоге того же репозитория
или в чужом репозитории — копия устаревает одинаково, разница только в том, насколько трудно это
заметить.
### Измеренный пример: YDB и восемь SDK
Самый наглядный случай, на котором задача и возникла. У YDB восемь официальных SDK, каждый в
отдельном репозитории:
| Язык | Репозиторий |
| --- | --- |
| Go | `ydb-platform/ydb-go-sdk` |
| Java | `ydb-platform/ydb-java-sdk` |
| Python | `ydb-platform/ydb-python-sdk` |
| C# | `ydb-platform/ydb-dotnet-sdk` |
| JavaScript | `ydb-platform/ydb-js-sdk` |
| Rust | `ydb-platform/ydb-rs-sdk` |
| C++ | `ydb-platform/ydb-cpp-sdk` |
| PHP | `ydb-platform/ydb-php-sdk` |
Документация — в девятом репозитории (`ydb-platform/ydb`, каталог `ydb/docs`), и примеры для всех
восьми SDK скопированы в неё руками. Масштаб на ветке `main`:
- 2350 файлов документации, из них 575 содержат код, всего 3178 код-блоков;
- 185 код-блоков на языках SDK в 59 файлах русской версии и ещё 179 в английской — около 360
скопированных вручную фрагментов;
- одна страница обычно показывает сценарий сразу во всех SDK: в рецепте аутентификации девять вкладок
(`Native SDK`, `Native SDK (Asyncio)`, `database/sql`, `JDBC`, `SQLAlchemy`, `userver` и другие).
Чтобы держать эти 360 фрагментов актуальными, авторам восьми SDK нужно помнить про девятый
репозиторий при каждом изменении примера. Дисциплиной это не решается.
### Другие сценарии того же класса
Задача не про SDK как таковые — они лишь понятный всем частный случай. Тот же механизм закрывает:
- **конфигурации и манифесты** — `docker-compose.yml`, k8s-манифесты, terraform, настройки CI,
которые показывают в разделах «как развернуть»;
- **сгенерированные артефакты** — OpenAPI-спеки, protobuf- и JSON-схемы, публикуемые сборкой;
- **тесты как документация** — интеграционный тест часто и есть самый честный пример использования,
и он уже поддерживается в рабочем состоянии;
- **миграции и SQL-скрипты**, которые применяются в проде и заодно показываются в документации;
- **код рядом с документацией** — файлы соседнего каталога или пакета в том же репозитории;
скачивать нечего, но копия устаревает точно так же;
- **общие фрагменты между наборами документации** — один и тот же файл переиспользуется доками
нескольких продуктов.
Общее у всех: у фрагмента есть оригинал, который поддерживают отдельно от документации.
## Предложение
Дать документации ссылаться на источник истины, а не на копию. Фрагмент вставляется директивой, а
физически берётся из внешнего источника на этапе сборки:
```
{% include-code [Подключение к БД](python-sdk:static-credentials/example.py#auth-static) %}
```
Источники объявляются один раз в `.yfm`:
```yaml
sources:
python-sdk:
type: git
repo: ydb-platform/ydb-python-sdk
ref: main
path: examples
infra:
type: local
dir: ../deploy
schemas:
type: http
url: https://storage.example.com/schemas
```
Фрагмент внутри файла размечается комментарием-маркером:
```python
# #region auth-static
driver_config = ydb.DriverConfig(
endpoint=endpoint,
credentials=ydb.StaticCredentials.from_user_password(user, password),
)
# #endregion auth-static
```
Результат в собранной документации — обычный код-блок: та же подсветка, та же кнопка копирования, а
под ним ссылка на исходный файл.
## Что это даёт
**Содержимое становится проверяемым.** Это главное. Сейчас код и конфиги в документации — текст,
который никто не компилирует и не валидирует. После перехода в документацию попадает фрагмент
реального файла, который живёт в своём репозитории под своим CI: пример компилируется и покрыт
тестами, манифест применяется, схема валидируется. Ответственность за работоспособность переезжает с
документатора на обычный пайплайн владельца файла.
**Расхождение становится заметным.** Если владелец источника удалит или переименует размеченный
фрагмент, сборка документации упадёт с явной ошибкой. Тихое протухание превращается в обычную поломку
сборки, которую видно сразу.
**Разделение ответственности.** Владельцы файлов поддерживают их у себя. Документаторы пишут контекст
и объяснения, не следя за содержимым фрагментов.
**Массовое переключение версий.** Версия источника указана в одном месте конфига, а не в сотнях
фрагментов: обновить все примеры — правка одной строки. Параметризация через переменные сборки
позволяет собирать одну документацию против разных версий источника.
**Ссылка на источник.** Под фрагментом — ссылка на файл, привязанная к конкретному коммиту (не к
подвижной ветке), с точными номерами строк. Читатель может перейти и посмотреть в полном контексте.
**Совместимость с офлайн-форматами.** Содержимое подставляется на этапе сборки, поэтому физически
попадает и в HTML, и в PDF. Никаких ссылок, которые невозможно разрешить офлайн.
## Дизайн
### Источники объявляются в конфиге, а не в директиве
Ключевое решение. Полный URL внутри каждой директивы (`{% include-code
"https://github.com/org/repo/blob/main/file.py" %}`) выглядит проще, но ломается ровно на том, ради
чего фича и нужна: обновление версии превращается в правку сотен вхождений, а ссылка на `main`
означает, что документация версии 23.x однажды начнёт показывать примеры из 26.x.
Поддерживаются три типа источников, тип указывается явно:
- **`git`** — файлы из репозитория на хостинге (`repo`, `host`, `ref`, `path`). Основной случай для
примеров и конфигураций из соседних репозиториев.
- **`http`** — обычные файлы по HTTP: объектное хранилище, артефакты сборки, публикуемые схемы
(`url`, `path`).
- **`local`** — каталог на диске: соседний пакет в монорепозитории, сгенерированный на этапе сборки
код, чекаут, который CI и так делает (`dir`, `path`).
Тип меняется без правки документов: в тексте стоит только логическое имя источника.
### Репозиторий не клонируется
Для `git`-источника ref резолвится в коммит по обычному HTTP (git smart HTTP, несколько килобайт),
после чего скачиваются только те файлы, на которые реально ссылаются документы.
Это принципиально для монорепозиториев: вытащить один файл из `ydb-platform/ydb` стоит килобайты
вместо десятков мегабайт, которых потребовал бы клон — даже поверхностный, blobless и sparse.
Из окружения нужен только доступ в сеть: `git` в PATH не требуется, ssh-ключи не нужны.
### Фрагменты адресуются именованными регионами, а не номерами строк
Номера строк протухают при первом же рефакторинге, причём молча. Маркеры регионов переживают правки и
являются явным контрактом между источником и документацией.
Маркер ищется в любом месте строки и не зависит от синтаксиса комментариев, поэтому одинаково работает
в Go, Python, Java, C#, JS, Rust, C++, PHP, а также в YAML, SQL и XML — то есть в конфигах и схемах,
а не только в коде. Поддерживаются две конвенции: `#region name` / `#endregion` и `[START name]` /
`[END name]` (принята в примерах Google). Строки-маркеры вырезаются из вывода, регионы можно
вкладывать, общий отступ снимается — фрагмент из середины файла выглядит самостоятельным.
Диапазоны строк (`#L10-L25`) поддерживаются для источников, владельцы которых не готовы добавлять
маркеры, но сборка предупреждает о них.
### Отсутствие региона — ошибка сборки
Сознательное решение: именно оно превращает тихое расхождение в громкий отказ в тот момент, когда
меняется исходник.
### Технические детали
- Директива раскрывается в обычный fenced-блок до остальной обработки markdown, поэтому HTML, PDF,
single-page, md2md, поиск и `llms.txt` работают без изменений.
- Ref резолвится один раз на сборку в главном потоке; файлы скачиваются по мере надобности, каждый
один раз, включая параллельную сборку (`-j N`).
- Скачанное складывается в каталог загрузки (`--sources-download-dir`) под именем коммита.
- Внешние файлы читаются через существующую песочницу путей, отдельным зарегистрированным scope, а не
в обход неё.
## Что потребуется от владельцев источников
Только одно: расставить маркеры регионов в тех файлах, на которые будет ссылаться документация.
Правка — две строки комментариев на фрагмент, файл при этом остаётся валидным и работающим.
Ссылка на файл целиком не требует вообще никаких изменений на стороне источника, поэтому переход
можно делать постепенно: сначала подключить источники и включать файлы целиком, а регионы просить
точечно, по мере надобности.
Contributor guide
Research direction
The issue names no implementation files or tests. Start by locating the YFM directive-processing entry point and the existing path-sandbox scope, then trace how build inputs and download options such as --sources-download-dir are handled. Done means git, HTTP, and local sources resolve regions into ordinary code blocks with errors for missing regions and source links preserved across build formats.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- typescript
- Domain
- build-system, cli, documentation
- Issue type
- Feature
- Difficulty
- 5/5
- Estimated time
- Over a week
- Activity status
- Active
- Clarity
- Mostly clear
- Newbie friendliness
- 35/100