arm-ka-fixtures/docs/superpowers/specs/2026-11-12-synthgen-design.md
Andrey Epifancev 004c21041c synthgen: сборка набора, README, детерминированность (#38)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-13 00:37:42 +04:00

305 lines
54 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Настраиваемый генератор синтетики ГСК — проект
Issue: RSHB/arm-ka-fixtures#38. Ветка `feat/38-synthgen`. Дата: 12.11.2026.
## 1. Зачем
Для ручного тестирования, демонстрации и проверки на объёме нужен набор первички группы компаний, который
**выглядит как выгрузки из 1С** и **внутренне согласован**: ОСВ по счетам, ББ и ОФР каждой компании построены из одних и
тех же проводок, поэтому сходятся между собой, а операции между компаниями группы дают пары ВГО и шахматку, на которые
есть что посмотреть. Набор должен генерироваться скриптом под нужный размер, а не собираться руками.
Реальные фикстуры (`osv/rb`, `osv/schastl`, `osv-dkb/`) — две-три компании, неполные периоды, реальные ИНН. Контрольный
набор #35 (`synthetic-group/`, 7 компаний, ручные сценарии, оракул) решает другую задачу — проверку алгоритмов на
известных случаях — и для объёма не годится: состав фиксирован, контрагентов единицы, оформление не похоже на 1С.
## 2. Цели, не-цели, анти-цели
**Цели**
- Один скрипт генерирует ОСВ, ББ, ОФР по всем компаниям и периодам и файл состава ГК для импорта ЕЦП.
- Семь настроек (§3) из конфигурационного файла с переопределением из командной строки.
- Файлы по виду — выгрузки 1С (§6).
- Данные согласованы (§5.6) и детерминированы: одинаковые настройки и `seed` → побайтно одинаковый набор.
**Не-цели (в этой задаче)**
- Оракул, ожидаемые результаты (`expected/`), ручные сценарии вида R-/P-/V-/G-/S- из #35.
- Сканы PDF и ответы ИС Сканирование.
- ББ/ОФР по ФСБУ 4/2023 и упрощённые формы — только 66н.
- Компании ГК вне ГСК, несколько ГК в одном прогоне, расшифровки, КФО клиента.
- Загрузка на стенд (для этого есть `load.py` в #35; приспособить его — отдельная задача).
**Анти-цели (провал, даже если «работает»)**
- Реальные ИНН или имена реальных организаций в наборе (в том числе остатки реквизитов из образцов 1С, §6.2).
- ОСВ, не сходящаяся с ББ/ОФР той же компании; ББ, где 1600 ≠ 1700.
- Набор, который парсер `tabular-parser` (ОСВ) или разбор ББ/ОФР в Excel не принимает из-за оформления генератора.
- Пересечение компаний двух прогонов с разными префиксами на одном стенде (имя или ИНН).
## 3. Настройки
Файл YAML (`--config`), любой ключ переопределяется `--set ключ=значение`. Неизвестный ключ или значение вне диапазона —
ошибка до начала генерации.
| # | Ключ | Смысл | По умолчанию | Диапазон |
|---|---|---|---|---|
| 1 | `group_size` | число компаний в ГК (все входят в ГСК) | 5 | 2–40 |
| 2 | `prefix` | префикс ГК: добавляется к названию группы и её компаний (§4.2), задаёт диапазон ИНН компаний группы | `SYN` | 2–12 символов, буквы/цифры, без пробелов |
| 3 | `periods` | число отчётных периодов | 4 | 1–12 |
| 4 | `start` | первый период, `ГГГГ-Qn` | `2025-Q1` | 2020-Q1 … 2030-Q4 |
| 5 | `osv_counterparties_mean` | среднее число **внешних** контрагентов в одной ОСВ (матожидание, §5.3) | 60 | 0–150 (ограничено словарём, решение 9) |
| 6 | `no_inn_companies` | сколько компаний группы выгружают ОСВ без ИНН контрагентов | 1 | 0–`group_size` |
| 7 | `vgo_mismatch_share` | доля пар ВГО с расхождением сторон | 0.1 | 0–1 |
| — | `seed` | зерно случайности | 1 | целое |
| — | `out` | каталог результата (или `--out`) | `./out/<prefix>` | — |
Пример: `uv run python -m synthgen --config synthgen/examples/default.yaml --set group_size=12 --set prefix=DEMO2 --out /tmp/demo2`.
## 4. Словарь компаний и состав группы
### 4.1 Словарь
Закоммиченный файл `synthgen/dictionary/companies.csv`: **400** вымышленных организаций —
`name;form;inn;kpp;kind` (`form` ∈ ООО/АО/ПАО/ИП; `kind` — отрасль для правдоподобия: агро, торговля, логистика,
производство, услуги, финансы, строительство). Около 10 % — ИП (ФИО + «ИП», ИНН 12 знаков, КПП пусто). Имена
собираются один раз детерминированным скриптом `synthgen/dictionary/build.py` из списков корней/отраслевых слов и
проверяются на совпадение с реальными: имена не должны совпадать с организациями из `fixtures/directories/` и фикстур ОСВ.
Из 400 записей **30 — банки** (`kind=банк`, АО/ПАО): они — кредиторы по 66/67 и только там.
ИНН — как в #35: регион `00` (не существует), верная контрольная сумма; номера словаря — диапазон `0100001–0109999`
(**не пересекается** с ИНН #35, где номера 1–99, решение 10).
Внешние контрагенты всех ОСВ берутся из словаря. Компании группы — тоже из словаря (§4.2), и **такая запись в этом
прогоне внешним контрагентом не бывает**.
### 4.2 Компании группы и префикс
- Из словаря выбираются `group_size` записей с `form` ∈ ООО/АО (не ИП); головная — АО.
- Имя компании группы: `<форма> "<prefix> <имя из словаря>"`, например `АО "DEMO2 Северная Нива"`. Название ГК: `ГК <prefix>`.
- ИНН компании группы — **не из словаря**, а из диапазона префикса: номер = `1000000 + 100 × h(prefix) + i`, где `h` —
детерминированный хеш префикса (SHA-256 → целое) в `0…89999`, `i` — порядковый номер компании (< 100). Так ИНН компаний
группы не совпадают с ИНН словаря, #35 и групп с другими префиксами (коллизия хеша ~1/90000; README её оговаривает).
- КПП — 9 цифр, как в #35: `00` + двузначный номер компании + `01001` (у ИП КПП нет).
## 5. Модель данных
### 5.1 Периоды
Периоды — `periods` кварталов подряд начиная с `start`. Каждый отчётный период — **нарастающим итогом с начала года**,
как выгружает 1С: Q1 → «1 квартал», Q2 → «1 полугодие», Q3 → «9 месяцев», Q4 → «год» (`ГГГГ-FY`). Пример: `start=2025-Q3`,
`periods=4` → `2025-Q3`, `2025-FY`, `2026-Q1`, `2026-Q2`.
Книга проводок компании ведётся с 1 января года первого периода (входящие остатки датированы **31 декабря предыдущего
года** проводкой против вспомогательного счёта `000`, как в #35) до конца последнего периода. ОСВ показывают `000` только
как источник входящих остатков — в файлы ОСВ счёт `000` не выгружается. Переход года: сальдо на 31.12 становится входящим остатком следующего
года, ОФР обнуляется закрытием 90/91 на 99 и 99 на 84 (реформация) в последний день года.
### 5.2 Экономика компании
Каждой компании назначается профиль из отрасли словаря и масштаб (выручка за год — логнормально, медиана ~1 млрд руб.,
разброс ×10 в обе стороны; головная — холдинг с небольшой выручкой и крупными вложениями 58.01). По профилю генерируются
помесячные операции:
- продажи (62/90), закупки (60/10/41/20), себестоимость (90.02), коммерческие и управленческие расходы (44/26);
- ОС и амортизация (01/02/08), денежные средства (51), НДС (19/68), налог на прибыль (68.04/99), зарплата (70/69) —
минимально, чтобы ББ был правдоподобным;
- банковские кредиты (66/67 с внешним банком из словаря), прочие расчёты (76), прочие доходы/расходы (91).
Деньги — `Decimal`, `ROUND_HALF_UP`, копейки в ОСВ; float только при записи в ячейку.
### 5.3 Внешние контрагенты в ОСВ
ОСВ генерируются по счетам **01, 58, 60, 62, 66, 67, 76, 80, 90** с субсчетами (§6.1): чек-лист `BASIC`
(01, 58, 60, 62, 66, 67, 76, 90 — `docs/context/01-project-overview.md`) плюс 80 (уставный капитал —
нужен правилу гудвилла 58.01 ↔ 80, ДП §4) (решение 7). Счета 01 и 90 по контрагентам не разворачиваются (01 — по
субсчетам, 90 — по субсчетам 90.01/90.02/…/90.09; у компании группы выручка от других компаний группы видна в ОСВ 62
продавца). 80 — по участникам (головная и миноритарий). Для каждой пары
(компания, счёт с контрагентами) генератор выбирает число внешних контрагентов `n` и набор `n` записей словаря; этот
набор постоянен по всем периодам компании (контрагент может иметь нулевое сальдо и обороты в отдельном периоде — тогда
строки нет, как в 1С).
`n` — отрицательное биномиальное с матожиданием `mean × w(счёт)` (дисперсия ≈ 2 × матожидание), где веса
`w`: 60 — 1.6, 62 — 1.6, 76 — 1.0, 66 — 0.1, 67 — 0.05, 58 — 0.1, 80 — 0 (только участники). Веса нормируются так,
что **среднее `n` по всем файлам ОСВ счетов 58, 60, 62, 66, 67, 76 = `osv_counterparties_mean`**. Потолки: 60/62/76 —
словарь без банков и компаний группы (≈ 370 − `group_size`), 66/67 — 30 банков. Если потолок срезал выборку, недобор
перераспределяется на остальные файлы того же счёта; тест среднего (допуск ±15 % при числе файлов ≥ 20) применяется,
когда потолок не сработал ни разу, иначе README прогона пишет «среднее ограничено словарём» (решение 9).
Обороты распределяются по контрагентам по Парето (несколько крупных, длинный хвост мелких), сумма = обороты счёта из
экономики §5.2. У части контрагентов (~20 %) только обороты без сальдо, у ~5 % — сальдо без оборотов (старая
задолженность). Знак сальдо по контрагенту — по смыслу счёта, у ~3 % в 60/62 — противоположный (аванс/переплата),
как в реальных ОСВ.
### 5.4 Операции внутри группы
- Головная компания владеет долями всех остальных: 58.01 у головной по каждой дочерней ↔ 80 у дочерней по головной
(уставный капитал; доля головной = её часть в 80). Доли 51–100 %, у одной-двух дочерних — миноритарий из словаря
(внешний участник, только в 80). Вклады — входящие остатки, движения в периодах нет.
- Торговля: каждая компания с вероятностью 0.5 продаёт каждой другой (62 у продавца ↔ 60 у покупателя); объём —
5–20 % выручки продавца; расчёты гасятся частично, на конец периода остаётся сальдо.
- Займы: головная выдаёт займы дочерним (58.03 ↔ 66.03/67.03), с процентами (76 ↔ 76, 91).
- Дивиденды: дочерние объявляют головной раз в год во 2 квартале (Дт 84 Кт 76.03 у плательщика ↔ Дт 76.03 Кт 91 у получателя — стороны правила ОФР 2310, ДП §4)
и **выплачивают в том же месяце** — на конец периода остатка нет, обороты видны в ОСВ 76 обеих сторон. Так дивиденды не
дают пар ВГО по сальдо и не создают ложных односторонних пар; дивиденды в расхождения §5.5 не попадают (решение 11).
- Каждая операция проводится **в двух книгах одновременно** симметрично (Дт у одной, Кт у другой — по смыслу счетов).
### 5.5 Расхождения ВГО
Пара ВГО — (компания A, компания B, вид расчёта: торговля / заём). Доля `vgo_mismatch_share` пар (округление вниз, но
не меньше 1, если доля > 0 и пар ≥ 1) получает расхождение: в последнем месяце **первого** периода года, где у пары есть
сальдо, одна сторона проводит накладную (или начисление процентов), которой нет у другой («в пути»). Величина — доля от меньшей стороны сальдо пары на конец этого периода, **вперемешку** (решение владельца 12.11.2026):
половина пар с расхождением (чётные по порядку выбора) — 1–4 % (ниже порога 5 % ДП §4: ядро сводит по `min`
автоматически), другая половина — 8–25 % (выше порога: пара подсвечивается и ждёт корректировки аналитика). При
нечётном числе пар лишняя — крупная. README прогона пишет для каждой пары процент и сторону порога. Расхождение держится до конца
года и снимается в январе следующего года проводкой второй стороны (накладная дошла) — в следующем году пара снова
выбирается заново. Остальные пары сходятся точно.
### 5.6 Согласованность (инварианты, проверяются тестами)
1. В каждой книге Σ Дт = Σ Кт по каждой проводке; сальдо `000` после входящих остатков — ноль.
2. ОСВ компании за период = свёртка её книги (сальдо на начало, обороты, сальдо на конец) по счёту/субсчёту/контрагенту;
строка родителя = сумма детей.
3. ББ = сальдо книги на конец периода по правилам свёртки счетов в строки 66н (из `forms.py` #35, §8); ОФР = обороты
90/91/99 за период нарастающим итогом. В тыс. руб. с `ROUND_HALF_UP` построчно, итоги — суммы округлённых строк;
1600 = 1700 обеспечивается подгонкой ±1 тыс. в одной строке (как `_plug` в #35). ОСВ — в рублях.
4. Пары ВГО без расхождения: сальдо у A по B = −(сальдо у B по A) по соответствующим счетам (62↔60, 58.03↔66.03/67.03,
76↔76); с расхождением — разница ровно на сумму накладной «в пути» §5.5 во всех периодах от её проводки до конца
года и ноль после января.
6. Колонка ББ «На 31 декабря» года, предшествующего первому периоду, = входящие остатки книги (= «Сальдо на начало»
ОСВ первого года); колонки ранее этого (ББ «на 31 декабря» позапрошлого года, ОФР за прошлый год, если прошлого года
нет в книге) — масштабированные значения профиля без требования согласованности (решение 5).
5. Компания группы в ОСВ другой компании группы записана тем же именем, что в ЕЦП, и тем же ИНН, кроме §6.1 (без ИНН).
## 6. Вид файлов
### 6.1 ОСВ
Формат «ОСВ по счёту» 1С 8.3 (БП 3.0) по образцу `fixtures/osv/rb/2024-Q1/сч 60_ 1 кв 2024 г.xlsx`: лист `Лист_1`,
строки 1–10 — шапка (организация, «Оборотно-сальдовая ведомость по счету NN за <период>», «Выводимые данные…»,
«Параметры: Валюта БУ, НУ: RUB», двухуровневый заголовок «Счет / Контрагенты», «Сальдо на начало периода / Обороты за
период / Сальдо на конец периода», «Дебет / Кредит»), строка 12 — счёт, далее субсчета (уровень группировки 1, отступ 1)
и контрагенты (уровень 2, отступ 2), внизу «Итого». Оформление — **как в образце**: Arial 8 (данные) / 9–10 (шапка) /
12 полужирный (заголовок), заливка шапки `F4ECC5`, строк счетов и субсчетов `F8F2D8`, тонкие рамки у всех ячеек
таблицы, фиксированные высоты строк, сетка узких колонок A…W с объединением ячеек (колонка «Счет» — A:D, суммы —
E:G, H:I, J:K, L:N, O:P, Q:… по образцу), формат `#,##0.00`, нулевые суммы — пустые ячейки, итог над детьми
(`summaryBelow=False`). Сетку колонок, ширины и объединения генератор берёт из образца программно (читает образец при
сборке шаблона), а не набирает вручную.
ИНН контрагента у компаний **с ИНН** — отдельная колонка после наименования (решение 3), у компаний из
`no_inn_companies` — колонки нет. Парсер ищет колонку ИНН строго между колонкой текста и первой колонкой сумм
(`tabular-parser/api/src/parser/detect.py`, `_inn_col`), поэтому в ОСВ с ИНН сетка образца **сдвигается**: наименование
A:D, ИНН — новая колонка E (ширина ~11), суммы — со следующей колонки с той же внутренней сеткой объединений, что в
образце. Шаблон хранит сетку как «блок текста + опциональная колонка ИНН + блок сумм», а не как фиксированные буквы. В ОСВ без ИНН компании группы записаны: большинство — тем же именем, что в ЕЦП
(автоматическое сопоставление по имени), **одна** компания группы на каждую такую ОСВ-компанию — немного другим
написанием (без кавычек и формы в конце, «Северная Нива DEMO2 ООО», как в 1С у реальных контрагентов) — её аналитик
сопоставит вручную (решение 4).
Имя файла: `сч NN_<период 1С>.xlsx`, например `сч 60_9 месяцев 2025 г.xlsx`, `сч 62_2025 г.xlsx`.
### 6.2 ББ и ОФР
Шаблон — реальная выгрузка 1С на листе `TDSheet` (форма 66н): `fixtures/statements-xlsx/mezheninovskaya/2025-Q3/
1_Форма1_бух_09.xlsx` (ББ) и `1_Форма2_бух_09.xlsx` (ОФР). Генератор **копирует книгу образца** (стили, объединения,
шрифты Times New Roman, высоты строк, рамки, формат нулей `[=0]"-";General`, блок «КОДЫ») и заменяет:
- реквизиты: наименование, ИНН, ОКПО, ОКВЭД, ОКОПФ/ОКФС, вид деятельности, адрес — вымышленные; **все** прочие ячейки
шапки с данными образца (аудитор, его ИНН/ОГРН, подписи, даты подписания) очищаются; тест проверяет, что ни одна строка
из образца, кроме неизменяемых текстов формы, не попала в файл;
- даты и заголовки колонок периодов («За 9 месяцев 2025 года», «На 31 декабря 2024 года», «На 31 декабря 2023 года» —
для ББ; текущий и прошлый год — для ОФР);
- значения — по кодам строк (колонка «Коды»), а не по номерам строк образца.
Образец правили в Excel, поэтому при сборке шаблона (`synthgen/templates/build_templates.py`, один раз, результат
коммитится) генератор: заменяет **все** формулы значениями (итоги 1100/1200/1600/1300/1400/1500/1700, 2100/…/2500 и
заголовки второй страницы — генератор пишет числа сам); удаляет рабочие формулы и значения вне формы (ячейки правее
последней колонки формы); очищает ФИО в подписях и ставит вымышленные; оставляет в ОФР строки 2530, 2900, 2910 (2530 —
из книги, если есть, иначе «-»; 2900/2910 — «-»). Значения в файле — в тыс. руб. **целыми** (решение 12), расходы ОФР —
**отрицательными** числами, как в образце (в скобках их не пишет и 1С). Набор строк — 66н из `forms.py` #35 плюс
2530/2900/2910; `edition_of` из #35 не копируется (всегда 66н).
Колонки сравнительных периодов (прошлый год / позапрошлый год), если книга их не покрывает (период до начала книги),
заполняются масштабированными значениями того же профиля (решение 5) — так у файла нет пустых колонок, но и
согласованность для них не требуется.
Имена файлов: `ББ_<период 1С>.xlsx`, `ОФР_<период 1С>.xlsx`.
### 6.3 Состав ГК (импорт ЕЦП)
`directories/ecp-<prefix>.xlsx` в формате файла #35 `synthetic-group/directories/ecp-synthetic-group.xlsx` (он
прошёл импорт в ядро; реальная выгрузка `fixtures/directories/ecp-export-russkiy-biskvit.xlsx` отличается — лист
`Выписки_01.04.26`, без КПП, имена «ЭКО ВОЛОГДА, АО» — импорт ядра принимает оба вида): лист `Выписки`, колонки
`ИНН, КПП, iD ГК, Базовый РФ ГК в CRM, Группа компаний, Организация, ГСК, ГСЗ, Тип`; все компании — «Входит в ГСК»;
`iD ГК` = `<prefix>-000001`; ИНН/КПП текстом. Перед реализацией исполнитель сверяет формат с эндпоинтом импорта
(`arm-core`, `POST /api/v1/directories/import`) и с `ecp.py` #35.
## 7. Результат прогона
```
<out>/
README.md — настройки прогона, состав группы (имя, ИНН, доля головной), список пар ВГО с
расхождениями (стороны, вид, сумма), какие компании без ИНН и какой контрагент
записан «другим именем», число контрагентов по файлам — не оракул, а журнал генератора
config.yaml — фактические настройки (с seed)
directories/ecp-<prefix>.xlsx
osv/<КОД>/<период>/сч NN_….xlsx
statements/<КОД>/<период>/ББ_….xlsx, ОФР_….xlsx
```
`<КОД>` — короткий латинский код компании (`C01`…`Cnn`, головная — `C01`); полное имя — в README и ЕЦП.
## 8. Код и переиспользование
- Пакет `synthgen/` в корне fixtures (рядом с `synthetic-group/`), запуск `uv run --with openpyxl==3.1.5 --with pyyaml
python -m synthgen …`. Тесты — `synthgen/tests/`, pytest.
- Ветка #35 ещё не слита, поэтому нужные модули **копируются** в `synthgen/` (с пометкой источника в docstring), не
импортируются из `synthetic-group/`: `inn.py`, `ledger.py`, `xlsx.py` (детерминированное сохранение), `chart.py`
(субсчета из seed arm-core; добавить недостающие — 80.09, 84, 91.09, 99 — сверив с
`arm-core` seed `chart_of_accounts.csv`), правила свёртки счетов в строки ББ/ОФР из `forms.py`, запись ЕЦП из `ecp.py`.
Источник — worktree `/Users/andrey.epifancev/projects/awp-ca/.wt/synthetic-group/fixtures/synthetic-group/gen/`.
Дедупликация — после слияния обоих (follow-up).
- Сгенерированные наборы **в git не коммитятся** (`out/` в `.gitignore`); коммитятся код, словарь, пример конфигурации и
шаблоны, собранные из образцов (без реквизитов образца).
## 9. Приёмка
- `pytest` зелёный: инварианты §5.6, распределение §5.3 (среднее в допуске), доля расхождений §5.5, число ОСВ без ИНН,
ИНН из диапазона префикса и с верной контрольной суммой, отсутствие реквизитов образцов, детерминированность
(два прогона с одним `seed` → побайтно равны; разный `seed` → различаются), ошибки конфигурации.
- Прогон по умолчанию и прогон «крупный» (`group_size=15, periods=6, osv_counterparties_mean=150`) проходят парсер ОСВ
`tabular-parser` (зеркальный worktree, как в #35) без ошибок; разбор ББ/ОФР в Excel находит все коды и сходит
1600 = 1700.
- Время крупного прогона — до 5 минут на ноутбуке разработчика.
## 10. Что оставлено исполнителю
Выбор распределений внутри указанных рамок, алгоритм генерации имён словаря, внутренняя структура пакета, формат
README прогона, точные субсчета внутри перечисленных счетов (из `chart.py`).
## Decisions made without discussion
| № | Дата | Решение | Обоснование | Цена ошибки |
|---|---|---|---|---|
| 1 | 12.11.2026 | Новый пакет `synthgen/` отдельно от `synthetic-group/` (#35), модули #35 копируются | #35 не слита; разные задачи (контрольный набор vs объём); импорт между каталогами связал бы их ветки | дублирование кода до follow-up |
| 2 | 12.11.2026 | Все компании ГК входят в ГСК; `group_size` — одно число | владелец назвал «размер ГСК/ГК» одной настройкой | нет сценария «член ГК вне ГСК» — добавить настройку позже |
| 3 | 12.11.2026 | ИНН — отдельная колонка после наименования | в образцах 1С ИНН нет; колонка уже принимается парсером (#35, приёмка tabular-parser); в имени ИНН сломал бы сопоставление по имени | если в реальных выгрузках ИНН иначе — поменять запись ОСВ |
| 4 | 12.11.2026 | В ОСВ без ИНН одна компания группы — с другим написанием имени | владелец: «аналитик сопоставит малую часть контрагентов» | если нужно больше ручных сопоставлений — сделать настройкой |
| 5 | 12.11.2026 | ББ «на 31.12» года перед первым периодом — из книги (входящие остатки); более ранние сравнительные колонки — масштабированные значения профиля | первая колонка должна совпадать с «Сальдо на начало» ОСВ того же набора; глубже книги нет, пустые колонки нетипичны для 1С | аналитик может заметить несоответствие позапрошлогодних цифр |
| 6 | 12.11.2026 | Только форма 66н | образец 1С с 66н есть (Межениновская, 9 мес. 2025); ФСБУ 4/2023 — не-цель | годовая отчётность 2025+ по новой форме не покрыта |
| 7 | 12.11.2026 | Счета ОСВ: 01, 58, 60, 62, 66, 67, 76, 80, 90 | чек-лист `BASIC` (01-project-overview) + 80 (правило гудвилла 58.01↔80); 02 не входит в `BASIC` | версия `EXTENDED` покажет пробелы чек-листа — добавить счета позже |
| 9 | 12.11.2026 | `osv_counterparties_mean` ≤ 150, потолки по словарю, перераспределение недобора | владелец задал словарь 200–400; при среднем 300 потолок 60/62 срезает выборку и среднее не выполняется | для большего объёма нужен словарь крупнее — расширить `build.py` |
| 10 | 12.11.2026 | ИНН словаря — номера 0100001–0109999, компании группы — 1000000+ по хешу префикса | не пересекаться с #35 (номера 1–99) и между префиксами на одном стенде | коллизия хеша префиксов ~1/90000 |
| 11 | 12.11.2026 | Дивиденды выплачиваются в месяце объявления, в пары ВГО по сальдо и расхождения не попадают | дивиденды проводятся через 76.03 у обеих сторон (стороны правила ОФР 2310) и гасятся в месяце объявления; остаток на дату ББ дал бы пару по 76.03 вне доли расхождений (ДП §4, правила 5–8) — остаток дал бы ложную одностороннюю пару | пары по дивидендам не видны в шахматке по сальдо (обороты видны в ОСВ 75/76) |
| 12 | 12.11.2026 | ББ/ОФР в тыс. руб. целыми, расходы ОФР отрицательными | так выглядит образец 1С; дробные тысячи в образце — следствие ручной правки | если ядро ждёт копейки в тысячах — поменять запись |
| 8 | 12.11.2026 | Значения по умолчанию §3 (5 компаний, `SYN`, 4 периода с 2025-Q1, 60 контрагентов, 1 без ИНН, доля 0.1) | разумный стартовый объём: «есть что смотреть», прогон быстрый | никакой — меняется настройкой |
| 13 | 12.11.2026 | Тесты жизненного цикла расхождений ВГО проверяют снятие 31 января, только если январь следующего года внутри периодов книги; иначе — только удержание до конца года (R-1) | книга кончается последним периодом, января после него нет | непроверенное снятие расхождения в хвосте набора |
| 14 | 12.11.2026 | Скрипт сборки шаблонов ББ/ОФР называется `build_forms_templates.py` (как в плане; §6.2 называл `build_templates.py`) (R-2) | отличать от `build_osv_style.py` | нет |
| 15 | 12.11.2026 | Префикс ГК — буквы латиницы и кириллицы + цифры, 2–12 символов (`config._PREFIX_RE`) | §3 говорит «буквы/цифры» без уточнения алфавита; название группы русское | префикс с кириллицей не пройдёт, если ядро ждёт только латиницу — сузить регулярное выражение |
| 16 | 12.11.2026 | Зерно `rng_for`/`stable_hash`: первые 8 байт SHA-256 читаются как big-endian | §8/§3 не фиксируют порядок байт; важна только стабильность между запусками | смена порядка меняет все наборы для того же seed одноразово — перегенерировать образцы |
| 17 | 12.11.2026 | `synthgen/dictionary/` — пакет с `__init__.py` (там `Org`, `load`), модуля `dictionary.py` нет | `python -m synthgen.dictionary.build` требует пакет; `dictionary.py` рядом с каталогом `dictionary/` давал бы конфликт имён; импорт `synthgen.dictionary` не меняется | нет |
| 18 | 12.11.2026 | Вариантное написание члена группы в ОСВ без ИНН: перестановка `<корень> <prefix> <форма>` без кавычек (`group.variant_spelling`); вариант выбирает `rng_for(seed, "group", prefix)` среди остальных членов | нужно написание, которое не совпадёт с `name` и `name_1c` посимвольно, но читается человеком; точный механизм сопоставления ядра по имени не проверялся | если ядро нормализует порядок слов и сопоставит автоматически — ручного сопоставления не будет, поменять правило варианта |
| 19 | 12.11.2026 | Доли головной: дочерняя без миноритария — `1.00`; у 1–2 дочерних (при `group_size=2` — у единственной) доля 0.51–0.99, миноритарий — внешняя организация словаря (юрлицо не банк) | §4 тестов требует `head_share ∈ [0.51, 1.00]` и миноритария у 1–2; миноритарий остаётся внешним контрагентом, источник долей — только данные группы | миноритарий не как контрагент в ОСВ — не будет связи, исключить из `externals` |
| 20 | 12.11.2026 | В ЕЦП «Базовый РФ ГК в CRM» — `Ростовский региональный филиал`, `Тип` — `ЮЛ`, как в #35; имя группы в колонке «Группа компаний» — `ГК <prefix>` (в #35 было без «ГК») | импорт ядра принимает любые значения (обязательны ИНН, iD ГК, Группа компаний, Организация, Тип); `ГК <prefix>` задано §4.2 | нет |
| 21 | 12.11.2026 | Веса контрагентов — детерминированная кривая Лоренца Парето (доля верхних p = p^(1−1/α), α=1.2, шум ±15 %), а не выборка `paretovariate` | выборка Парето на 43 контрагентах давала долю верхних 10 % от 23 % до 97 % по зерну; тест формы (≥ 50 %) стал бы случайным | распределение чуть «глаже» настоящей выборки Парето |
| 22 | 12.11.2026 | `Ledger`: индекс накопленных итогов по (счёт, аналитика) для `balance` на дату не раньше последней проводки; `Posting`/`An` — `slots=True` | закрытие месяца спрашивает сальдо по 90/91/68/99 после каждого месяца; полный проход по проводкам давал O(N²) на крупном прогоне (600 тыс. проводок при 15 компаниях, 6 периодах, 150 контрагентах) | запрос сальдо на прошлую дату всё ещё идёт проходом по проводкам (кэшируется) |
| 23 | 12.11.2026 | В `chart.ACCOUNTS` добавлены 01.01, 67.01, 67.02; названия всех субсчетов сверены с seed arm-core (`api/seed/core/chart_of_accounts.csv`) — расхождений нет (T1.7) | экономика использует 01.01 и кредиты/проценты 67.01/67.02, которых не было в копии #35 | нет |
| 24 | 12.11.2026 | Поведение контрагентов: ~72 % «обычные» (оплата в следующем месяце), ~20 % «только обороты» (оплата в месяце), ~5 % «только сальдо» (входящий остаток), ~3 % «аванс» (62.02/60.02 обратного знака, не меньше одного при n ≥ 8); трое крупнейших — всегда обычные. Для 58 «обычные» и «только сальдо» — остаток займа 58.03, «только обороты» — выдан и возвращён в месяце. Без внешних контрагентов (mean = 0) операции идут без аналитики контрагента | §5.3 задаёт доли, но не привязку к субсчетам; механика — из #35 | доли в ОСВ могут отличаться от реальной выгрузки банка — поменять константы в `economy._classes` |
| 25 | 12.11.2026 | Головная в `economy` — холдинг: выручка ~50 млн ₽/год, ОС 1.5× выручки, закупок 10 %; вложений 58.01 и участников в 80 нет — 80 проводится без аналитики на 2 % годовой выручки (их добавит Task 4 вместе с операциями внутри группы) | вложения 58.01 по смыслу — операции внутри группы (гудвилл 58.01 ↔ 80 дочерних), не внешняя экономика | Task 4 должен перепровести 80 по участникам и 58.01 головной |
| 26 | 12.11.2026 | Входящие остатки групповых операций (Task 4): капитал дочерней — 80.09 по участникам (головная `An(cp=<код>)`, миноритарий `An(cp=<ИНН>)`, доли по `head_share`), у головной 58.01 по каждой дочерней на ту же сумму; займы головной — 58.03 ↔ 66.03 или 67.03 (по зерну `loan-term`) на 3–6 % годовой выручки дочерней; 84 пересчитывается из итога строк после хука (`economy.open_books(opening_hook=…)`). Собственный 80 головной остаётся без аналитики | §5.4 говорит «80», бриф — «80.09»; по ДП §4 правило гудвилла берёт 80/83, 80.09 входит; участники головной в группе неизвестны | если ядру нужен 80 (без .09) — сменить субсчёт в `_opening_hook` |
| 27 | 12.11.2026 | `VgoPair` помимо `mismatch` получил `mismatches` (по одному на год, где пара выбрана); `mismatch` — расхождение первого года | §5.5 выбирает пары заново каждый год, одним полем не выразить | потребители читают `mismatches`; `mismatch` — только для удобства |
| 28 | 12.11.2026 | Торговля: объём пары 5–20 % выручки продавца (с НДС), но суммарные закупки покупателя у группы ≤ 10 % его годовой выручки (пропорциональное сжатие); счёт в день 10, оплата 50–90 % в месяце счёта, остаток — в следующем; себестоимость продавца по ВГО-реализации не проводится, покупатель списывает товар (90.02 Кт 41) | без ограничения покупатель с 7 поставщиками уходил в минус по 51 (проверяется тестом); себестоимость продавца потребовала бы остатков 41 | прибыль продавца по ВГО завышена на себестоимость; объём при многих продавцах меньше 5 % |
| 29 | 12.11.2026 | Расхождение: процент малых — 1.05–3.95, крупных — 8.05–24.95 (первые `k//2` выбранных малые); проводка «в пути» — день 25, снятие — 31 января; у торговли — накладная продавца (Дт 62.01 Кт 90.01) или покупателя (Дт 41 Кт 60.01), у займа — транш по основному долгу (Дт 58.03 Кт 51 у займодавца либо Дт 51 Кт 66.03/67.03 у заёмщика; R-3, решение 32). `pct` считается от меньшей стороны сальдо на конец периода создания; в следующих периодах сальдо торговли меняется, сумма разницы остаётся | границы с запасом от 1/4/8/25 %, чтобы округление суммы не выводило процент из диапазона | пересчёт процента ядром по другому правилу даст иной pct |
| 30 | 12.11.2026 | Дивиденды: май каждого года книги, 3 % годовой выручки дочерней; доля головной по `head_share` (Дт 84 Кт 76.03 `An(cp=<код>)` ↔ Дт 76.03 Кт 91.01 item «Дивиденды»), остаток — миноритарию через `An(cp=<ИНН>)`; выплата в день 20 того же месяца | решение 11 требует нулевого сальдо 76.03; миноритарий получает свою долю | если нужны дивиденды из прибыли года — привязать к 84 |
| 31 | 12.11.2026 | Проценты по займам группы — 10 % годовых, начисляются и уплачиваются 28-го числа каждого месяца по субсчёту 76.09 обеих сторон | пары 76↔76 по правилу 6 ДП §4, остаток 76.09 нулевой | нет |
| 32 | 12.11.2026 | R-3 (рулинг контроллера по ревью Task 4): расхождение по займу — по основному долгу (правило 4 ДП §4), проценты 76.09 ↔ 76.09 начисляются симметрично и гасятся в месяце начисления, в расхождения не попадают; если у займодавца на конец периода денег меньше 2 × суммы транша, транш «в пути» проводит заёмщик. ВГО-выручка продавца за год ≤ 40 % его годовой выручки с НДС (`SELLER_CAP`, симметрично `BUYER_CAP`; пропорциональное сжатие) | расхождение в 76↔76 дало бы ядру правило 6 с меньшей стороной 0 (процент не определён, OQ-175); без потолка продавца ВГО-выручка могла превысить годовую выручку | при другом проценте ядра у займов оракул Task 5 расходится с генератором; потолок 40 % сжимает объём пар |
| 33 | 12.11.2026 | Свёртка ОСВ (`osv.rows`) идёт по индексу книги с накопленными по дням итогами (счёт, субсчёт, контрагент), а не через `Ledger.balance/turnover` на каждый период; результат совпадает с ними (тест) | на каждый период и счёт проходить по всем проводкам книги слишком долго | нет: расхождение ловится `test_rows_match_ledger` |
| 34 | 12.11.2026 | Строки ОСВ без сальдо и оборотов не пишутся; у счёта 80 без аналитики (головной) строка счёта без раскрытия; «Итого» — сумма строк счёта; колонка ИНН пишется только у контрагентов с ИНН | как в выгрузке 1С; ДП не требует нулевых строк | нет: парсер принимает, тест `-m parser` |
| 35 | 12.11.2026 | T3.8: закрывающие проводки различаются полем `Posting.kind` (`KIND_CLOSE_90` — 90.01..90.08 → 90.09, `KIND_CLOSE_99` — 90.09 и 91 → 99, `KIND_CLOSE_COST` — 26/44 → 90.08/90.07), memo остаётся подписью. В ОФР не входят `close90` и `close99`; закрытие затрат входит; обороты считаются как «нетто» Дт − Кт по счёту, а не только по одной стороне | разбор memo хрупок; закрытие на 90.09 удваивало бы обороты 90.01/90.02 | проводка без `kind` в закрывающем коде попадёт в ОФР: ловит `test_net_profit_equals_99_balance` |
| 36 | 12.11.2026 | Масштабированные колонки ББ (до начала книги): множитель 0.8–1.1 на строку из `rng_for(seed, "prev", код, вид, код компании, период, номер колонки)` (спека называла только код и вид — добавлены компания, период и колонка, чтобы колонки не совпадали между компаниями); баланс уравнивает строка 1370 (нераспределённая прибыль), а не ±1 подгонка | независимые множители ломают 1600 = 1700 на величину, не лечащуюся подгонкой ±1; прошлые колонки по спеке согласованности не требуют | 1370 прошлых колонок может выйти нетипичной (в т. ч. убыток) |
| 37 | 12.11.2026 | Подгонка округления ББ: ±1 тыс. строкам с наименьшей потерей точности, каждой строке не более одного раза; подогнанные строки записаны в `Form.plugged` (по колонкам) | «в одной строке» из §5.6 п. 3 не всегда хватает: при ~20 строках расхождение округлений достигает нескольких тыс. | строка подгонки может отличаться от книги на 1 тыс. (тест допускает ±1 только для `plugged`) |
| 38 | 12.11.2026 | ОФР: 2411 = 2410 (налог текущий), 2412 = 0, 2460/2510/2520/2530/2900/2910 = «-»; 2500 = 2400 + 2510 + 2520 + 2530; подписи колонок: «За 3 месяца / 6 месяцев / 9 месяцев\n<год> года», для года — «За <год> год» (ОФР) и «На 31 декабря\n<год> года» (ББ); дата шапки — конец периода | отложенных налогов и прочих результатов модель не создаёт; подписи по образцу 1С, парсер определяет даты колонок | OPEN-QUESTION: форма подписей «За 3 месяца» для 1 квартала у реальных выгрузок не проверена |
| 39 | 12.11.2026 | В шаблонах ББ/ОФР сохранены флаги образца (`M15`/`R15` ББ, нумерация колонок ОФР); реквизиты (ОКПО, ОКВЭД по отрасли, ОКОПФ 65/47, адрес, ФИО подписантов) — детерминированные вымышленные из `stable_hash` по ИНН/компании/периоду | формы 66н требуют реквизитов; ничего из образца не должно попасть в набор | ОКПО/ОКОПФ вымышлены, не проверяются классификаторами |
| 40 | 13.11.2026 | Сборка набора: пустые по правилу 1С ОСВ тоже пишутся (шапка и «Итого» с нулями), число файлов не зависит от экономики; каталог результата по умолчанию `./out/<prefix>`; `--force` очищает содержимое `out` целиком; в `config.yaml` и README `out` всегда `null` (путь результата в файлы не попадает) | детерминированность побайтно при разных `--out`; ожидаемое число файлов предсказуемо (компании × периоды × 9 счетов) | `--force` стирает всё в `out` — указывать только каталог набора; пустой файл ОСВ может смутить загрузчик, ждущий непустых |