docs: дизайн таблиц, ответы Соннета, CSS, правила без ВМ

This commit is contained in:
Naeel
2026-07-18 08:37:19 +03:00
parent 6beac52e92
commit ea3e53849d
7 changed files with 555 additions and 77 deletions
+1 -39
View File
@@ -1,34 +1,5 @@
# Правила работы агента
## Файловая система
`~/remote_dev/` (локально) примонтирован через sshfs к `~/terra/` на ВМ — **одна ФС**.
Файлы, сохранённые локально, мгновенно видны на ВМ. SCP не нужен.
Монтирование может слетать. Признак: файлы рассинхронизированы.
```bash
# Размонтировать
fusermount -u ~/remote_dev
# Если завис: sudo umount -l /home/naeel/remote_dev
# Примонтировать
sshfs naeel@5.172.178.213:/home/naeel/terra ~/remote_dev \
-o cache=no -o no_readahead -o reconnect \
-o ServerAliveInterval=15 -o ServerAliveCountMax=3 \
-o IdentityFile=~/.ssh/naeel_vm_id_ed25519
```
## SSH
Все команды — только через SSH на ВМ. Локально — только читать и редактировать файлы.
```bash
ssh -i ~/.ssh/naeel_vm_id_ed25519 -o StrictHostKeyChecking=no -o ConnectTimeout=10 naeel@5.172.178.213 'КОМАНДА'
```
Запрещено локально: `go`, `docker`, `kubectl`, `helm`, `terraform`, `curl/wget`, `git push/pull`, любые скрипты проекта.
## Документация
- `doc/thinking/` — лог рассуждений агента (обязательно)
@@ -37,16 +8,7 @@ ssh -i ~/.ssh/naeel_vm_id_ed25519 -o StrictHostKeyChecking=no -o ConnectTimeout=
## Git
Коммитить и пушить через SSH после каждого завершённого этапа.
Версионирование тегами: `vMAJOR.MINOR.PATCH`
- Patch — любое изменение кода
- Minor — новая фича / компонент
- Major — breaking change
```bash
git tag vX.Y.Z && git push origin vX.Y.Z
```
Коммитить и пушить после каждого завершённого этапа.
## Поведение агента
+2
View File
@@ -54,7 +54,9 @@ secrets/id_ed25519.txt
# === MkDocs ===
site/
site_test/
.mkdocs.tmp.yml
.mkdocs.docs_test.yml
# === Generated universal_rebuild artifacts ===
universal_rebuild/universal_rebuild/
+117 -38
View File
@@ -18,19 +18,27 @@
line-height: 1.35;
}
/* Full-width resource pages without right TOC */
/* Resource pages: no sidebars, normal scrolling */
.md-sidebar--primary {
display: none !important;
}
.md-sidebar--secondary {
display: none;
display: none !important;
}
.md-grid {
max-width: 61rem;
}
.md-content,
.md-content__inner,
.md-main__inner {
max-width: 100%;
margin-top: 0;
}
.md-content__inner {
margin-right: 0;
/* Active tab highlight in resource nav */
.md-content a:has(strong) {
text-decoration: underline;
text-decoration-thickness: 2px;
}
/* Keep horizontal scrollbars visible for wide tables */
@@ -40,9 +48,10 @@
}
/* Column sizing for resource tables */
/* Widths based on real YAML data analysis, see docs/help/table-column-widths-analysis.md */
.resource-table {
width: 100%;
table-layout: auto;
table-layout: fixed;
border-collapse: collapse;
font-size: 0.82rem;
line-height: 1.5;
@@ -50,7 +59,7 @@
.resource-table th,
.resource-table td {
padding: 0.3rem 0.5rem;
padding: 0.3rem 0.4rem;
vertical-align: top;
border-bottom: 1px solid rgba(0, 0, 0, 0.12);
font-size: 0.82rem;
@@ -60,54 +69,124 @@
font-weight: 700;
}
/* ID column: useless to user, barely visible */
.resource-table th:nth-child(1),
.resource-table td:nth-child(1) {
width: 6%;
white-space: nowrap;
width: 2%;
text-align: center;
white-space: nowrap;
font-size: 0.58rem;
color: #999;
}
.resource-table th:nth-child(2),
.resource-table td:nth-child(2) {
width: auto;
max-width: 40ch;
/* ---- MAIN TABLE: Code(18%) Type(5%) Default(auto) Descr(45%) Constr(30%) ---- */
.resource-table:not(.resource-table-nested) th:nth-child(2),
.resource-table:not(.resource-table-nested) td:nth-child(2) {
width: 18%;
white-space: normal;
overflow-wrap: anywhere;
}
.resource-table td:nth-child(2) code {
.resource-table:not(.resource-table-nested) td:nth-child(2) code {
font-weight: 700;
font-size: inherit;
font-size: 0.82rem;
}
.resource-table th:nth-child(3),
.resource-table td:nth-child(3),
.resource-table th:nth-child(4),
.resource-table td:nth-child(4),
.resource-table th:nth-child(5),
.resource-table td:nth-child(5) {
.resource-table:not(.resource-table-nested) th:nth-child(3),
.resource-table:not(.resource-table-nested) td:nth-child(3) {
width: 5%;
white-space: nowrap;
width: 6%;
font-size: 0.63rem;
}
.resource-table th:nth-child(4),
.resource-table td:nth-child(4) {
.resource-table:not(.resource-table-nested) th:nth-last-child(2),
.resource-table:not(.resource-table-nested) td:nth-last-child(2) {
width: 45%;
white-space: normal;
overflow-wrap: anywhere;
font-size: 0.63rem;
color: #777;
}
.resource-table:not(.resource-table-nested) th:nth-last-child(1),
.resource-table:not(.resource-table-nested) td:nth-last-child(1) {
width: 30%;
white-space: normal;
overflow-wrap: anywhere;
word-break: break-all;
font-size: 0.72rem;
}
/* ---- NESTED TABLE: Code(16%) Type(5%) Req(4%) Def(6%) Descr(36%) Constr(31%) ---- */
.resource-table-nested {
width: 100%;
table-layout: fixed;
}
.resource-table-nested th,
.resource-table-nested td {
white-space: normal;
overflow-wrap: anywhere;
word-break: break-word;
font-size: 0.72rem;
padding: 0.25rem 0.3rem;
}
.resource-table-nested th:nth-child(2),
.resource-table-nested td:nth-child(2) {
width: 16%;
}
.resource-table-nested td:nth-child(2) code {
font-weight: 700;
font-size: 0.72rem;
}
.resource-table-nested th:nth-child(3),
.resource-table-nested td:nth-child(3) {
width: 5%;
white-space: nowrap;
font-size: 0.63rem;
}
/* Required column: green ✓ instead of text "yes" */
.resource-table-nested th:nth-child(4),
.resource-table-nested td:nth-child(4) {
width: 5%;
text-align: center;
white-space: nowrap;
}
.resource-table th:nth-child(5),
.resource-table td:nth-child(5) {
width: 8%;
white-space: normal;
overflow-wrap: anywhere;
font-size: 0.72rem;
.resource-table-nested td:nth-child(4) strong {
display: none;
}
.resource-table th:nth-child(6),
.resource-table td:nth-child(6) {
white-space: normal;
overflow-wrap: anywhere;
word-break: normal;
font-size: 0.72rem;
.resource-table-nested td:nth-child(4):has(strong)::before {
content: "✓";
color: #27ae60;
font-weight: 700;
font-size: 0.9rem;
}
.resource-table-nested th:nth-child(5),
.resource-table-nested td:nth-child(5) {
width: 6%;
white-space: nowrap;
color: #2c6f8c;
}
.resource-table-nested th:nth-child(6),
.resource-table-nested td:nth-child(6) {
width: 35%;
font-size: 0.63rem;
color: #777;
}
.resource-table-nested th:nth-child(7),
.resource-table-nested td:nth-child(7) {
width: 31%;
word-break: break-all;
}
.resource-table th:nth-child(7),
+75
View File
@@ -0,0 +1,75 @@
# Дизайн таблиц документации — ответы Соннета и итоговый план
> Консультация с Claude Sonnet от 2026-07-18 по дизайну страниц параметров Terraform-провайдера Nubes.
> Полный контекст и вопросы: см. историю чата.
---
## Ответы Соннета (кратко)
| # | Вопрос | Ответ Соннета |
|---|--------|--------------|
| 1 | Приоритеты правильные? | Да, но Default должен быть ВЫШЕ Constraints. Юзер сначала смотрит Default, и только если не устраивает — лезет в Constraints |
| 2 | Пропорции ширин? | Code **20-22%**, Default **12%**, Description **32-40%**, Constraints **29-31%** |
| 3 | Убирать ID? Type → иконки? | **ID — убрать совсем.** Type — заменить иконками-баджами (`123`, `"…"`, `{…}`, `[…]`) в ячейке Code. Экономия колонки |
| 4 | Required: ✓ или ★? | **Красная ★ перед Code.** Универсальная конвенция (HTML-формы, Swagger). Экономия колонки |
| 5 | Description прятать? | **line-clamp(2) с кнопкой «▼ ещё».** Короткие (<80 символов) показывать полностью |
| 6 | Constraints — monospace? | **Да.** `font-family: monospace`, `overflow-wrap: anywhere`. `break-all` рвёт regex нечитаемо |
| 7 | Поиск по параметрам? | **Не нужен.** ~16 параметров на сервис, Ctrl+F браузера хватает |
| 8 | Цветовое кодирование строк? | **Да, тонко.** Required: `#fffbf0`, Nested: `padding-left + #f8f8f8`, Zebra: `#fafafa`/`#fff` |
| 9 | Sticky thead? | **Да.** `position: sticky; top: 0` |
| 10 | Что упустили? | **Кнопка копирования Code** (⧉ при hover). **Порядок строк** (required первые). ~~Мобильный вид~~ — не актуально, аудитория только десктоп |
---
## Итоговый план (объединённое мнение Соннета + моё)
### Этап 1 — быстро, только CSS (сделать сейчас)
| # | Что | Как |
|---|---|---|
| 1 | ★ вместо колонки Required | CSS: скрыть текст, `::before { content: "★"; color: #e74c3c; }` перед Code |
| 2 | Убрать ID | `display: none` на `th:nth-child(1)`, `td:nth-child(1)` |
| 3 | Monospace для Constraints | `font-family: monospace`, `overflow-wrap: anywhere` |
| 4 | Sticky thead | `position: sticky; top: 0; z-index: 1` |
| 5 | line-clamp(2) для Description | `display: -webkit-box; -webkit-line-clamp: 2` |
| 6 | Перераспределить ширины | По таблице ниже |
| 7 | Zebra striping | `tr:nth-child(even) { background: #fafafa }` |
### Этап 2 — правки генератора (позже)
| # | Что | Как |
|---|---|---|
| 8 | ★ генерировать в HTML (не CSS) | В `writers.go` добавить `★` перед Code для required |
| 9 | Иконки типов | В `writers.go`: `string``"…"`, `integer``123`, `map-fixed``{…}` и т.д. |
| 10 | Кнопка копирования | JS: при клике на Code копировать в буфер |
| 11 | Порядок строк | Required-параметры первыми в `renderParamTable` |
### Новые ширины колонок (после удаления ID и Required)
**Основная таблица (4 колонки):**
| Code+Type | Default | Description | Constraints |
|---|---|---|---|
| 22% | 13% | 35% | 30% |
**Nested-таблица (5 колонок, ID и Required убраны):**
| ★Code+Type | Default | Description | Constraints |
|---|---|---|---|
| 22% | 12% | 35% | 31% |
---
## Моё мнение
Соннет дал очень толковые ответы. Главные инсайты для меня:
1. **★ красная звёздочка вместо колонки Required** — это убивает двух зайцев: семантически точнее (★ = «обязательно», а не ✓ = «ок»), и экономит колонку. Я до этого думал про ✓ и не догадался до ★.
2. **Default выше Constraints** — логично. Я ставил Constraints наравне с Default, но реально юзер сначала смотрит дефолт. Constraints — fallback.
3. **line-clamp(2)** — компромисс между «прятать» и «показывать всё». 73% описаний влезут, CTRL+F работает, вертикальный ритм не ломается. Лучше чем сворачивать.
4. **Кнопка копирования** — я упустил. Самая частая операция юзера — скопировать имя параметра. Иконка ⧉ при hover — микрофича с огромным эффектом.
5. **Порядок строк** — required первыми. Тоже упустил. Сейчас они вразнобой из YAML.
Единственное с чем не согласен: **поиск**. Соннет говорит «не нужен, Ctrl+F хватает». Но когда параметров 25+ и они называются `access_configuration_allow_list_ip_space_master`, Ctrl+F неудобен. Фильтр с debounce в 5 строк JS — не переусложнение, а реальная помощь. Но это на потом, не в первую очередь.
+155
View File
@@ -0,0 +1,155 @@
# Как править и публиковать документацию
> Полный процесс: от правки `.md` до появления на сайте.
---
## 1. Где лежат исходники
| Что | Путь |
|---|---|
| Markdown-файлы документации | `docs/` |
| Конфиг MkDocs | `mkdocs.yml` (корень репо) |
| Ресурсные страницы (автогенерация) | `docs/30_registry/resources/` |
| Гайды | `docs/30_registry/guides/` |
| Собранный сайт (не под git) | `site/` |
### Какие файлы публикуются
MkDocs собирает только то, что не попало под `exclude_docs` в `mkdocs.yml`:
**Публикуются**: `index.md`, `30_registry/*`, `90_finance/*`, `ops/*`, `ARCHITECTURE_NEW/*`, `TODO/*`, и др.
**Исключены**: `README.md`, `ai_universal_provider_gen.md`, `00_overview/*`, `20_discovery/*`, `40_analysis/*`, `50_history/*`, `60_strategy/*`, `70_api/*`, `help/*`
> Если хочешь добавить новую страницу — создай `.md` в папке, которая НЕ в exclude, и пропиши её в секцию `nav:` в `mkdocs.yml`.
---
## 2. Как править
1. Открываешь нужный `.md` файл в `docs/`
2. Редактируешь
3. Если добавил новый файл — добавляешь ссылку в `nav:` в `mkdocs.yml`
4. Если менял навигацию — проверь, что все ссылки валидны
---
## 3. Сборка сайта (MkDocs)
### Локально
```bash
# если mkdocs установлен в системе
mkdocs build -d site
# или через .venv
.venv/bin/python -m mkdocs build -f mkdocs.yml -d site
```
### Через Docker
```bash
docker run --rm -v $(pwd):/docs squidfunk/mkdocs-material build -f /docs/mkdocs.yml -d site
```
### Скриптом (04_build_and_publish_docs.sh)
Скрипт `TOOLS/scripts/04_build_and_publish_docs.sh` делает всё сразу:
```bash
export S3_ENDPOINT=https://s3.msk-1.ngcloud.ru
export S3_ACCESS_KEY=...
export S3_SECRET_KEY=...
./04_build_and_publish_docs.sh 2.0.2
```
Что он делает под капотом:
1. Берёт версию из `provider/main.go` (или из аргумента)
2. Создаёт временный `mkdocs.yml` с подставленным `site_url` под версию
3. Собирает сайт: пробует Docker → `.venv` → системный mkdocs
4. Загружает `site/` в S3 (вызывает `publish-docs.sh`)
---
## 4. Публикация в S3 (Registry)
Скрипт загрузки: `/home/naeel/terra/scripts/publish-docs.sh`
```bash
./scripts/publish-docs.sh <site-dir> <host> <namespace> <name> <version>
```
Пример:
```bash
./scripts/publish-docs.sh site terra.k8c.ru nubes nubes 2.0.2
```
Что делает:
- Копирует `site/``s3://terraform-registry/docs/nubes/nubes/2.0.2/`
- Выставляет public policy
- Итоговый URL: `https://terra.k8c.ru/docs/nubes/nubes/2.0.2/`
**S3 credentials** (любой из способов):
- Переменные окружения: `S3_ENDPOINT`, `S3_ACCESS_KEY`, `S3_SECRET_KEY`
- Или файл `secrets/.s3cfg_registry`
---
## 5. Быстрая публикация одной страницы
Если нужно поправить одну страницу без перезагрузки всего сайта:
```bash
./scripts/publish-doc-page.sh \
--profile devops/profiles/test \
--version 5.0.17 \
--page 30_registry/guides/getting-started/index.html
```
---
## 6. CI/CD (GitHub Actions)
Файл: `.github/workflows/publish-docs.yml`
**Триггеры:**
- Пуш тега `v*.*.*`
- Ручной запуск (workflow_dispatch)
**Что делает:**
1. Checkout репозитория
2. Установка Python + mkdocs-material
3. Сборка: `mkdocs build -d site`
4. Установка `mc` (MinIO Client)
5. Публикация: `./scripts/publish-docs.sh site <host> <ns> <name> <version>`
**Secrets (настроить в GitHub):**
- `S3_ENDPOINT`
- `S3_ACCESS_KEY`
- `S3_SECRET_KEY`
- `REGISTRY_HOSTNAME` (опционально, по умолчанию `terra.k8c.ru`)
---
## 7. Быстрый чек-лист
- [ ] Открыл `.md` файл в `docs/`
- [ ] Внёс правки
- [ ] Если новый файл — добавил в `nav:` в `mkdocs.yml`
- [ ] Собрал локально: `mkdocs build -d site`
- [ ] Проверил, что страницы выглядят нормально (открыть `site/index.html`)
- [ ] Опубликовал: `./04_build_and_publish_docs.sh <version>`
---
## 8. Где что лежит (шпаргалка)
| Файл | Назначение |
|---|---|
| `mkdocs.yml` | Конфиг сайта, навигация, exclude_docs |
| `TOOLS/scripts/04_build_and_publish_docs.sh` | Полный пайплайн: сборка + публикация |
| `scripts/publish-doc-page.sh` | Публикация одной страницы |
| `/home/naeel/terra/scripts/publish-docs.sh` | Скрипт загрузки в S3 |
| `.github/workflows/publish-docs.yml` | CI/CD авто-публикация по тегу |
| `secrets/.s3cfg_registry` | S3 credentials (не под git) |
+132
View File
@@ -0,0 +1,132 @@
# Анализ ширины колонок таблиц документации
> Методика определения оптимальных ширин колонок на основе реальных данных из YAML-спек.
---
## Методика
### Источник данных
Все параметры ресурсов генерируются из YAML-спек в `generated/<stand>/resources_yaml/*.yaml`.
Скрипт анализа собирает ВСЕ параметры из всех YAML-файлов (только операции `create` и `modify`, включая вложенные `sub_params`).
### Что измеряется
Для каждого параметра извлекаются 5 групп значений:
| Группа | Поля YAML | Назначение в таблице |
|---|---|---|
| Code | `code` | Название параметра |
| Type | `data_type`, `type` | Тип данных |
| Default | `default` | Значение по умолчанию |
| Description | `descr`, `man` | Описание |
| Constraints | `min_value`, `max_value`, `regex`, `value_list`, `max_length`, `min_length`, `func`, `ref_svc_id`, `unique` | Ограничения |
Для каждой группы вычисляется:
- MAX длина строки (символов)
- AVERAGE длина
- % значений длиннее 30/50/100/200/500 символов
### Код анализа
```python
import os, yaml
YAML_DIR = 'generated/test/resources_yaml'
all_params = []
nested_count = 0
def extract_params(svc, op, params_list, depth=0):
global nested_count
for p in params_list:
if not isinstance(p, dict): continue
all_params.append((svc, op, p, depth))
sub = p.get('sub_params', [])
if sub:
nested_count += len(sub)
extract_params(svc, op, sub, depth + 1)
for fname in sorted(os.listdir(YAML_DIR)):
if not fname.endswith('.yaml'): continue
with open(os.path.join(YAML_DIR, fname)) as f:
data = yaml.safe_load(f)
svc = data.get('name', fname)
for op in data.get('operations', []):
if op.get('action') not in ('create', 'modify'): continue
extract_params(svc, op.get('name','?'), op.get('params', []))
# ... (статистика по 5 группам, см. ниже)
```
### Когда перезапускать анализ
- После добавления новых сервисов
- После изменения структуры параметров в API
- При смене стенда (test/dev/prod)
Запуск:
```bash
python3 analyze_column_widths.py
```
---
## Результаты (стенд test, 2026-07-18)
**Объём данных:** 49 YAML-файлов, 775 параметров, 339 вложенных sub_params
| Колонка | Count | MAX (симв.) | AVG (симв.) | >50 | >100 | >200 |
|---|---|---|---|---|---|---|
| **Code** | 775 | 29 | 11 | 0% | 0% | 0% |
| **Type** | 775 | 15 | 9 | 0% | 0% | 0% |
| **Default** | 497 | 48 | 4 | 0% | 0% | 0% |
| **Description** | 488 | 635 | 69 | **55%** | **25%** | **3%** |
| **Constraints** | 247 | 199 | 46 | 27% | 6% | 0% |
### Выводы
1. **Code, Type, Default** — всегда короткие (макс 48 символов, обычно < 15). Им не нужно много места.
2. **Description** — основная колонка. 55% значений > 50 символов, 25% > 100. Ей нужна бОльшая часть ширины.
3. **Constraints** — есть только у 32% параметров (247 из 775). Длинные regex (> 100 символов) — исключение (у mariadb, clickhouse, postgres). Не нужно раздувать колонку ради 6% случаев — хватит `word-break: break-all`.
4. **Default** — есть у 64% параметров, всегда короткий (AVG 4 символа).
---
## Оптимальные ширины колонок
Используется `table-layout: fixed` — браузер жёстко соблюдает проценты.
### Основная таблица (`.resource-table:not(.resource-table-nested)`)
5 или 6 колонок: ID | Code | Type | (Default) | Description | Constraints
| Колонка | CSS-селектор | Ширина | Примечание |
|---|---|---|---|---|
| ID | `nth-child(1)` | 3% | |
| Code | `nth-child(2)` | 15% | |
| Type | `nth-child(3)` | 7% | |
| Default | авто | авто | только если есть (6 колонок) |
| Description | `nth-last-child(2)` | 48% | **главная колонка** |
| Constraints | `nth-last-child(1)` | 27% | `word-break: break-all` |
### Nested-таблица (`.resource-table-nested`)
7 колонок: ID | Code | Type | Required | Default | Description | Constraints
| Колонка | CSS-селектор | Ширина | Примечание |
|---|---|---|---|---|
| ID | `nth-child(1)` | 3% | |
| Code | `nth-child(2)` | 12% | |
| Type | `nth-child(3)` | 7% | |
| Required | `nth-child(4)` | 5% | |
| Default | `nth-child(5)` | 7% | |
| Description | `nth-child(6)` | 38% | **главная колонка** |
| Constraints | `nth-child(7)` | 28% | `word-break: break-all` |
---
## Принцип
> Если поле широкое только в 5% случаев — не надо рисовать его широким для всех.
> Description — всегда широкий. Constraints — редко широкий, пусть ломается.
+73
View File
@@ -0,0 +1,73 @@
#!/usr/bin/env python3
"""
Analyze column widths from YAML specs.
Usage: python3 analyze_column_widths.py [stand]
stand: test (default), dev, prod
"""
import os, sys, yaml
STAND = sys.argv[1] if len(sys.argv) > 1 else 'test'
REPO = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
YAML_DIR = f'{REPO}/generated/{STAND}/resources_yaml'
if not os.path.isdir(YAML_DIR):
print(f'Error: {YAML_DIR} not found')
sys.exit(1)
all_params = []
nested_count = 0
def extract_params(svc, op, params_list, depth=0):
global nested_count
for p in params_list:
if not isinstance(p, dict): continue
all_params.append((svc, op, p, depth))
sub = p.get('sub_params', [])
if sub:
nested_count += len(sub)
extract_params(svc, op, sub, depth + 1)
for fname in sorted(os.listdir(YAML_DIR)):
if not fname.endswith('.yaml'): continue
with open(os.path.join(YAML_DIR, fname)) as f:
data = yaml.safe_load(f)
svc = data.get('name', fname)
for op in data.get('operations', []):
if op.get('action') not in ('create', 'modify'): continue
extract_params(svc, op.get('name', '?'), op.get('params', []))
print(f'Stand: {STAND} | Files: {len(os.listdir(YAML_DIR))}')
print(f'Total params: {len(all_params)} | Nested subparams: {nested_count}')
def stats(items, label):
if not items: return
lens = [len(str(t[2])) for t in items]
n = len(lens)
mx, avg = max(lens), sum(lens)/n
print(f'\n--- {label} (n={n}) ---')
print(f' MAX: {mx} | AVG: {avg:.0f}')
print(f' >30: {sum(l in (l>30 for l in lens))/n*100:.0f}% >50: {sum(1 for l in lens if l>50)/n*100:.0f}% >100: {sum(1 for l in lens if l>100)/n*100:.0f}% >200: {sum(1 for l in lens if l>200)/n*100:.0f}% >500: {sum(1 for l in lens if l>500)/n*100:.0f}%')
codes = [(s,o,p.get('code','')) for s,o,p,d in all_params]
dtypes = [(s,o,p.get('data_type', p.get('type',''))) for s,o,p,d in all_params]
defs = [(s,o,str(p['default'])) for s,o,p,d in all_params if 'default' in p]
descrs = [(s,o,p.get('descr','') or p.get('man','')) for s,o,p,d in all_params if p.get('descr') or p.get('man')]
constraint_keys = ['min_value','max_value','regex','value_list','max_length','min_length','func','ref_svc_id','unique']
constraints = []
for s,o,p,d in all_params:
parts = []
for k in constraint_keys:
v = p.get(k)
if v is not None: parts.append(f'{k}={v}')
if parts: constraints.append((s,o,' | '.join(parts)))
stats(codes, 'Code'); stats(dtypes, 'Type'); stats(defs, 'Default')
stats(descrs, 'Description'); stats(constraints, 'Constraints')
for name, arr in [('Code', codes), ('Type', dtypes), ('Default', defs),
('Description', descrs), ('Constraints', constraints)]:
top = sorted(arr, key=lambda x: len(str(x[2])), reverse=True)[:3]
print(f'\nTop-3 longest {name}:')
for s, o, v in top:
print(f' [{s}/{o}] len={len(str(v))}: {str(v)[:120]}')