Ресурсное планирование · service/docs

Действующие требования собраны в спецификации; отдельное дополнение читать не требуется. Расчётный эталон 5213150; прежние редакции сохранены как история. Исторические материалы отмечены отдельно; рабочие подключения остаются предложениями, QA прототипа завершена 13.10.2026.

service/docs/CONTEXT.md

Словарь предметной области

Редакция 13.10.2026 по дополнению NEW-01–09 и реализации прототипа 5213150. Расчётное поведение ниже принято для прототипа; инфраструктура, авторизация, интеграции и хранение рабочего сервиса остаются предложениями. Общая QA всех NEW ещё предстоит. Расчётный контракт, матрица покрытия.

Источник названий — ТЗ v1.7 §1 с приоритетным дополнением 13.10; расчётная база — prototype-v2 5213150. Имена ниже — единые имена будущих контрактов и кода; UI использует русский термин. role без уточнения не применять: ресурсная роль не даёт прав доступа.

Термин Имя Значение и границы
Заявка Request Просьба РП на один Project; содержит ResourceLine, статус pending/approved/returned/rejected, автора и версии.
Строка ресурса ResourceLine Запрошенная ResourceRole, необязательный Employee, одна доходная ставка и MonthPlan. Стабильный lineId заменяет индекс массива.
Назначение Assignment Согласованная строка с employeeId, projectId, managerId, доходной ставкой и снимком расходной; независимый assignmentId, исходный requestId/lineId, версия.
Изменение назначения AssignmentChange Отдельная Request с kind=change: release/reduce/replace, effectiveDate, reason, before и proposed; до решения не влияет на план.
Руководитель проекта ProjectManager / pm Пользователь, которому разрешён проект; request.managerId сохраняет принадлежность заявки конкретному РП.
Ресурсный менеджер ResourceManager / rm Право принимать решения и видеть общий ресурсный план. Не должность, не владелец группы Bitrix.
Сотрудник Employee Единый человек пула со стабильным ID и external identity; деактивация не удаляет историю.
Пользователь Principal Авторизованная личность; связана с Employee через подтверждённый ID, но не через совпадение ФИО/email.
Роль ресурса ResourceRole Шесть ролей seed.js: бизнес-аналитик, системный аналитик, архитектор, Backend, Frontend, тестировщик. Расширение и сопоставление — Q-07.
Право доступа AccessGrant pm/rm и область проектов; отдельное право viewCost для расходных данных. Не импортируется из WORK_POSITION.
Проект Project Стабильный локальный ID, имя, внешняя сущность и ID, закрытие, связи РП. Имя — подпись, не ключ.
Подразделение Department Внешний ID, имя, parentId; несколько принадлежностей сотрудника возможны, основное для отчёта — Q-07.
План месяца MonthPlan month YYYY-MM, fte и необязательные days; явный 0 в days важнее месячного значения.
Плановая загрузка plannedFte Доля полной загрузки, P/H; сумма для группы — FTE, не количество людей.
Фактическая загрузка actualFte Для ручного ввода 0…2; при импорте часов производное A/H; не равна плану по умолчанию.
Норма часов одного сотрудника H normHours Персональная H = Hкал − объединённые рабочие часы отпуска и больничного. Неизвестна, если нет подтверждения любого вида отсутствия.
Календарная норма Hкал calendarHours Сумма date→hours федерального производственного календаря. Отдельна от персональной H.
Больничный B sickLeaveHours Тот же формат и полнота, что отпуск. Общие даты учитываются один раз.
Полнота отсутствий AbsenceCoverage Для сотрудника×месяца нужны отпуск и больничный; пусто не равно 0.
Доступные часы дня availableDayHours Часы календаря минус объединённые датированные отсутствия; сводные часы распределяются пропорционально.
Плановые часы P plannedHours FTE×персональная H; при days — сумма дневного FTE×доступные часы дня. Отдельной нормы продажи нет.
Доходная ставка R incomeRate Положительные ₽/ч, одна на строку и период.
Расходная ставка C costRate ₽/ч специалиста; при решении сохраняется costRateSnapshot, источник и версия.
Плановый доход I plannedIncome P×R, не подтверждённая выручка.
Плановый расход plannedCost P×C.
Превышение расходной ставки Δ costExcess max(C−R,0); не путать с incomeRate−costRate.
Средние ставки Rср/Cср weightedIncomeRate / weightedCostRate Взвешены плановыми часами, при нуле часов неизвестны.
Разница ставок / средних ставок rateDifference / weightedRateDifference R−C / Rср−Cср.
Разница плановых сумм plannedDifference plannedIncome−plannedCost.
Превышение планового расхода за период excessPlannedCost Δ×ΣP отдельной строки.
Фактические часы A actualHours Работы из Time или отдельно подтверждённого ручного факта; отпуск исключён.
Норма часов группы N groupNormHours Σ персональных H выбранных сотрудников/месяцев. Неполнота любой H делает N неизвестной.
Часы отпуска V vacationHours Employee×month, сводные рабочие часы 0…Hкал либо полные даты; независимо от проекта. Не часы работ.
Коммерческая плановая утилизация commercialUtilization Pком/N×100; те же люди и фильтры, только коммерческий числитель.
Сумма FTE за период periodFte Сумма месячных FTE, единица FTE·мес; процент периода — ΣP/ΣH.
Плановая/фактическая утилизация по норме plannedUtilization / actualUtilization P/N×100 и A/N×100, индивидуально знаменатель H.
Свободные часы / свободный ресурс freeHours / freeFte Σmax(H−P,0) / Σmax(1−plannedFte,0).
Доля отпуска vacationShare V/ΣHкал×100; при общей доле разных отсутствий использовать объединение, не сумму пересекающихся V и B.
Полнота факта ActualCoverage Покрытая область людей, проектов, дат и версия источника; отсутствие записи без полного покрытия неизвестно.
Журнал решений DecisionLog Неизменяемые автор, время, действие, before/after и версии; не технический лог приложения.
Снимок календаря CalendarVersion Версионированный набор date→hours; основание расчёта, не личный график сотрудника.
Сопоставление ExternalIdentity Уникальная тройка source + entityType + externalId → localId; email/название не ключ.

Единицы и совместимость

Дата бизнеса — календарная дата Europe/Moscow; timestamp журнала — UTC instant. День изменения включён в новый период. Статус участия ended вычисляется по Clock, не хранится cron-обновлением.

Часы/FTE/ставки — точные десятичные значения; деньги — копейки или decimal. Формулы не округляют промежуточные результаты. Предложение: rates numeric(18,2), FTE numeric(12,8), hours numeric(20,8), денежный результат decimal с округлением до копейки при представлении; утверждение масштаба и округления — Q-11. JSON decimal передаётся строкой, домен не вычисляет деньги через JS float.

Неизвестно — null плюс причина missing/stale/unmapped, а не 0. В существующей месячной planSummary пустая выборка → 0%; кольца utilizationIndicators и процент периода при N=0 → неизвестно. Hкал остаётся видимой при неизвестной H. Подробнее — calculation-contract.md.

Адаптация прототипа: RequestItem → Request; Line → ResourceLine; targetId+lineIndex → assignmentId с таблицей соответствий при импорте; имена project/manager → projectId/managerId; months[].rate → взвешенная incomeRate. days сохраняется. Демо seed.js остаётся источником сценариев, не справочником рабочего сервиса.

Словарь будущего сервиса использует стабильные ID и Decimal; прототип остаётся на JS Number и именах проектов, localStorage/revision. Эти проектируемые изменения не реализованы документальной редакцией.

service/docs/addendum-coverage.md

Прослеживаемость NEW-01–09

Редакция по дополнению 13.10.2026 и фактическому prototype-v2 5213150. Все девять реализованы принятыми пакетами; эта матрица связывает требования с существующими файлами/проверками, но не объявляет общую QA и риск-ревью завершёнными. Исходный v1.7 и старые картинки сохранены как история. Расчётный контракт, спецификация, проверки, будущий сервис.

Все пути к реализации и тестам ниже — относительно prototype-v2, документы — относительно service/docs. Unit — Node npm test; browser-файлы — функции для Playwright на опубликованной странице. Проектируемые L1–L5/HTTP-интеграции сервиса не считаются этими тестами выполненными.

NEW Принятое поведение / REQ Код и владелец Проверки существующего прототипа Документ
NEW-01 Доход/расход/разница периода; отрицательная красная, ≥0 зелёная, неизвестная C прочерк; REQ-009 js/calc.js requestFinance; js/views/request-modals.js financialTotal tests/addendum-ui.test.js, tests/personal-norm.test.js, tests/browser-addendum.js, tests/browser-personal-norm.js; 88 ч →352000/378400/−26400, 0, неизвестность, личная H spec, calculation-contract: P и деньги; analysis/design-base §7
NEW-02 Расход после Дохода в месячных таблицах формы/рассмотрения/просмотра; отрицательная разница выделяет расход; REQ-009 js/views/request-modals.js lineCard → calc.requestFinance tests/addendum-ui.test.js, tests/browser-addendum.js; сумма месячных строк=периоду spec, calculation-contract; analysis/design-base §7
NEW-03 Производственный календарь, H сотрудника, полнота отпуск/больничный, union дат, P=FTE×H, N=ΣH; REQ-008/024/026/027 js/calc.js, js/api.js, js/seed.js, js/ui.js, js/views/data-modals.js; потребители плана/отчётов/денег/изменений tests/personal-norm.test.js, tests/data.test.js, tests/browser-personal-norm.js, tests/browser-calendar-boundary.js; 24 нормы, H=152/0/null, суммы/атомарность/рабочая суббота/январь–март spec, CONTEXT, calculation-contract, architecture, data-flows F4, ADR-0006, Q-06
NEW-04 Четыре верхние вкладки и пять разделов Отчётов; собственное состояние/действия сохранены; REQ-018/022/023 js/ui.js, js/app.js, js/views/index.js, js/views/reports.js tests/navigation.test.js, tests/addendum-ui.test.js, tests/browser-addendum.js; переключения, РМ/РП, фильтры, кнопки spec REQ-018, analysis/design-base §3; calculation-contract
NEW-05 Σ меняет единицу; FTE·мес суммирует месячные FTE, % взвешен нормами; REQ-011/026 js/calc.js periodTotal/planPeriod; js/views/plan.js tests/aggregates.test.js, tests/browser-aggregates.js; 2,25, 388/512, unknown/zero spec, CONTEXT, calculation-contract; analysis/design-base §4
NEW-06 Нижний Итог без повторного учёта проектов, неизменный при свёртке, по фильтрам/РП; REQ-011/026 js/calc.js planTotal; js/views/plan.js tests/aggregates.test.js, tests/browser-aggregates.js; поиск имени/проекта/свободного ресурса/scope, collapse spec, calculation-contract; analysis/design-base §4
NEW-07 Расход после Дохода в списке, отрицательная разница красная, неизвестные суммы прочерк; REQ-009 js/views/requests.js, js/views/shared.js → calc.requestFinance tests/addendum-ui.test.js, tests/browser-addendum.js, tests/browser-personal-norm.js; список=карточка, личная H spec, calculation-contract; analysis/design-base §7
NEW-08 Коммерческое плановое кольцо с общей группой/нормой, ограничен числитель; REQ-018 js/calc.js utilizationIndicators; js/views/reports.js, styles.css tests/aggregates.test.js, tests/browser-aggregates.js; 256/1040, фильтры, неизвестная/нулевая H spec, calculation-contract; analysis/design-base §8
NEW-09 Фактическое кольцо A/N, неполнота/нулевая/неизвестная N → прочерк с причиной; REQ-018/027 js/calc.js actualHours/reportPeriod/utilizationIndicators; js/views/reports.js; seed полного ноября tests/aggregates.test.js, tests/browser-aggregates.js, tests/personal-norm.test.js; 477/954=50%, partial/null/zero spec, calculation-contract, testing; analysis/design-base §8

Отдельный принятый старый дефект: ЦФО/подразделение и CSV — js/views/reports.js, tests/distribution.test.js, tests/browser-addendum.js, REQ-020. Это не десятое требование дополнения.

Следующая общая QA

Повторить все связанные browser-сценарии на одном опубликованном состоянии, РМ/РП и 1440/320/375/390 px; сверить все NEW, отсутствие JS-ошибок, скролл таблиц, полноту/нули и денежные представления. Результаты прежних пакетов и количество unit-тестов не подменяют эту приёмку. Документы service описывают будущие подключения и авторизацию, а не готовый рабочий сервис.

service/docs/adr/0001-data-master.md

ADR-0001: Мастер заявок и назначений

Статус: предложено, 13.10.2026; к реализации после утверждения spec и относящихся Q.

Контекст

INT 03 не определяет мастер. Два независимых writer создадут конфликт между локальной транзакцией, историей и сущностью Bitrix.

Решение

Предложить локальный Postgres как единственный мастер Request/Assignment/DecisionLog. Сфера мастер справочников, Time мастер импортированного факта, 1С календаря/ставок после Q. Живой запуск этой модели требует Q-02.

Последствия

Атомарное решение и журнал доступны без сети. Источники связаны ExternalIdentity; ручные дополнения отдельно. Если нужен Bitrix-master, заменить persistence port недостаточно: пересмотреть транзакции/компенсации и spec. Для копии в Bitrix возможен отдельный outbox, сейчас не реализуется.

Отвергнутые варианты

Dual-write без outbox теряет согласованность; хранить всё одной JSON State блокирует независимые записи; Bitrix-master не выбран без реестра и утверждённой модели.

service/docs/adr/0002-sync-worker.md

ADR-0002: Явный синк и отдельный worker

Статус: предложено, 13.10.2026; к реализации после утверждения spec и относящихся Q.

Контекст

INT 02/04 требуют повторяемого синка и сохранения истории при сбоях. задача №215 задаёт SphereClient и ручной/15-минутный запуск. Nitro tasks остаются experimental.

Решение

Один модуль DirectorySync.run для теста/ручного вызова/worker. Production: отдельный процесс того же Node-релиза, задания/lease/fencing в Postgres, 15 минут как предложение. Локальная PGlite: единственный процесс с ручным runner, cron выключен.

Последствия

HTTP не держится на долгом импорте; ошибки/повторы/полнота наблюдаемы. Нужно эксплуатировать второй процесс и задания; раздельных бизнес-сервисов и очереди Redis нет. Порт источника не знает scheduler; replay и crash проверяются.

Отвергнутые варианты

Nitro scheduledTasks проще, но привязаны к платформе/экспериментальному API и жизненному циклу web; in-memory cron теряет задание; отдельные микросервисы/брокер пока не оправданы. Пересмотр допустим при стабильном Nitro scheduler, сохраняющем тот же run.

service/docs/adr/0003-secrets.md

ADR-0003: Серверные секреты интеграций

Статус: предложено, 13.10.2026; к реализации после утверждения spec и относящихся Q.

Контекст

INT 04: URL входящего webhook сам является секретом, scope зависит от прав его владельца. Клиентский config и логи не могут его раскрывать.

Решение

Private runtimeConfig получает secrets из env секретного хранилища среды. В репо только .env.example с пустыми значениями. Для sync — отдельный служебный read-only аккаунт/приложение с минимальными scopes; transport allowlist методов чтения как дополнительный барьер. OAuth client secret и session key отдельно; refresh tokens при хранении шифруются ключом вне БД.

Последствия

Ротация требует обновления secrets/restart и проверки read smoke. Редактировать scope и выдавать credentials может оператор по регламенту; бухгалтерские ставки не загружаются целиком без необходимости. Структурные логи/Sentry удаляют headers, query tokens, webhook paths, тела профилей/ставок. Тесты проверяют отсутствие утечек.

Отвергнутые варианты

Токены в public runtimeConfig/браузере/коммитах запрещены; один админский webhook на все интеграции избыточен; шифрование ключом из той же таблицы БД не защищает backup.

service/docs/adr/0004-identity-access.md

ADR-0004: Идентификация отдельно от прав

Статус: предложено, 13.10.2026; к реализации после утверждения spec и относящихся Q.

Контекст

NFR 01 и задача №215 используют вход Сферы; возможен корпоративный SSO. Владение группой/должность не доказывают роль РМ.

Решение

Рекомендовать Bitrix OAuth code flow через IdentityProvider; корпоративный OIDC — альтернативный adapter после Q-08. VerifiedIdentity связывается по issuer+subject/portal+userId, далее серверные AccessGrant и opaque session cookie. viewCost отдельно, по умолчанию deny.

Последствия

Нужно согласовать grant/revoke/TTL/freshness и перенос портфеля. Проверка scope до запросов/агрегаций/CSV, revoke при деактивации. Test identity только NODE_ENV=test; production-negative тест обязателен, SPHERE_FAKE не открывает вход.

Отвергнутые варианты

Имена/email и client mode=rm не идентификация. Sync webhook не логин пользователя. Копировать default member и fake auth задача №215 без усиления нельзя: другая матрица прав и требование test-only.

service/docs/adr/0005-stack-modules.md

ADR-0005: Стек и глубокие модули

Статус: предложено, 13.10.2026; к реализации после утверждения spec и относящихся Q.

Контекст

Нужна ручная поддержка и совпадение со задача №215; UI прототипа — vanilla mock, а ТЗ описывает ещё более раннюю Cloudflare-версию.

Решение

TypeScript strict, Node 22 LTS, pnpm, Nuxt 4 UI+Nitro, zod/shared DTO, Postgres/Drizzle, PGlite локально/тесты. Шесть глубоких модулей из architecture.md, чистый Calculations и заменяемые ports. Vitest/fast-check/Playwright.

Последствия

Один язык и репозиторий; тонкие handlers, перенос смысла calc.js без seed globals; поддержка Postgres concurrency тестов отдельно от embedded БД. Патч-версии фиксировать в П1 с lockfile; проверять совместимость Nuxt test-utils.

Отвергнутые варианты

Микросервисы усложнят транзакции и диагностику; generic слои репозиториев на каждый метод раздувают интерфейсы; повтор Cloudflare D1/State main и переключателя ролей не соответствует рабочему контуру.

service/docs/adr/0006-versioned-data.md

ADR-0006: Снимки решений, календаря и факта

Статус: предложено, 13.10.2026; к реализации после утверждения spec и относящихся Q.

Контекст

NFR 02 требует исходных и согласованных значений. Справочники/календари меняются, Time исправляет часы; повторная сумма приводит к дрейфу отчётов.

Решение

Формулы дополнения приняты: H = календарь минус объединённые рабочие часы отпуска/больничного, P=FTE×H, N=ΣH; отдельной нормы продажи нет. Расчётный контракт фиксирует полноту и допущения actuals/отсутствий прототипа 5213150.

Рабочая архитектура по-прежнему предлагается: исходный Request, версии Assignment и append-only DecisionLog сохраняются транзакционно; CostRateSnapshot при решении; CalendarVersion и AbsenceSnapshot/Coverage — явные входы расчёта. Time entries upsert по ID/version с coverage, без повторного сложения; Decimal вместо JS Number. В текущем прототипе этих серверных таблиц/транзакций нет.

Последствия

Даты отсутствий объединяются до норм; нет двойного вычета. Обе записи vacation/sick обязательны для известной H, пусто не 0. Сводные рабочие часы без пересечений распределяются по календарю пропорционально только в демо; точные дни задаются dates. Реальная схема/полнота источника — Q-06.

Изменение текущих введённых отсутствий в прототипе меняет P и деньги уже согласованной заявки. Новая политика исторического пересчёта, замораживания денег или пересогласования рабочего сервиса этим ADR не утверждается и не реализуется. Срок хранения/удаления — Q-09. Исходные месячные ставки сохраняются в дневных изменениях; их миграция рабочего сервиса возможна лишь отдельным принятым импортом.

Отвергнутые варианты

Чтение текущей C для всех прошлых решений меняет историю; округление до суммирования и замена unknown на 0 дают неверные итоги; event sourcing всего приложения избыточен — достаточно версий агрегатов и журнала решений.

service/docs/architecture.md

Архитектура рабочего сервиса

Редакция 13.10.2026 по дополнению NEW-01–09 и реализации прототипа 5213150. Расчётное поведение ниже принято для прототипа; инфраструктура, авторизация, интеграции и хранение рабочего сервиса остаются предложениями. Общая QA всех NEW ещё предстоит. Расчётный контракт, матрица покрытия.

Статус: предложение к утверждению. Требования — spec.md; решения — adr/; открытые продуктовые развилки — open-questions.md. Схемы — diagrams.html (самодостаточная страница с SVG, без внешних скриптов).

Основание и совместимость со задача №215

ТЗ §30: INT 01 — адаптер сущности проектов; INT 02 — неразрушающий синк; INT 03 — один мастер назначений; INT 04 — серверные секреты. NFR 01 — область доступа до чтения данных; NFR 02 — транзакции и журнал; NFR 03 — наблюдаемость и согласуемые восстановление/ёмкость.

задача №215 прочитана 13.10.2026: Nuxt 4/Nitro, TypeScript, Node 22, pnpm, Postgres/Drizzle, PGlite без DATABASE_URL, zod, vitest/Playwright. Её SphereClient и интервал SPHERE_SYNC_MINUTES=15 — общий контракт, пока не доказано наличие готовой библиотеки. Не объявляем скопированный интерфейс переиспользованным кодом: при реализации сначала найти реализацию и контрактные фикстуры. Модель Team, степени access и предупреждение FTE>1 из «Итераций» сюда не переносятся: ТЗ стаффинга требует блокировку перегрузки и роли РП/РМ. Фейковый вход задача №215 по SPHERE_FAKE сужен здесь до NODE_ENV=test.

Репозиторий service создан отдельно, remote не задан по условию задачи; до правок каталог отсутствовал. Проверены локальные ветки prototype-v2: main, t-12345-data/screens/shell, архитектурной ветки нет; remote отсутствует, поиск MR неприменим. История задача №232 и задача №215 пуста. Это новая архитектурная постановка, не исправление неудачной реализации.

C4 L1: контекст

flowchart LR
  PM[Руководитель проекта] -->|Заявка, изменение, свои отчёты| S[Сервис ресурсного планирования]
  RM[Ресурсный менеджер] -->|Решения, общий план| S
  S -->|Чтение справочников и проектов| B[Сфера / Bitrix24]
  S -->|Чтение фактических часов| T[Time Лоции]
  S -->|Чтение календаря и возможных ставок| C[1С]
  S -->|Вход и проверка личности| I[Bitrix24 OAuth или корпоративный SSO]
  O[Технический оператор] -->|Состояние синка, восстановление| S

Пользователи не обращаются к интеграционным API напрямую. OAuth обмен и служебный REST-синк — разные подключения. Time не считается стандартным методом Bitrix24. Ни один поток не пишет в Сферу (рекомендация по INT 03).

C4 L2: контейнеры

flowchart TB
  U[Браузер: Nuxt UI] -->|HTTPS, cookie, общие DTO| A[Nitro API: доступ и модули]
  A -->|Транзакции и проекции| D[(Postgres)]
  W[Фоновый Node worker: тот же пакет модулей] -->|Задания, lease, checkpoint| D
  A -->|Ручной запуск: запись задания| D
  W -->|Порты чтения| E[Bitrix24 / Time / 1С]
  A -->|Порт идентификации| I[OAuth / SSO]
  A --> L[Логи, метрики, Sentry]
  W --> L
  D --> B[Резервные копии и проверка восстановления]

Один репозиторий и один модульный монолит. В production API и worker — два процесса одного релиза с Postgres. Worker выбран вместо experimental Nitro scheduledTasks (ADR-0002); Redis и брокер сообщений не нужны. Scheduler создаёт задания в БД, worker обрабатывает их с lease и повтором после падения. При локальной PGlite только один владелец БД — API-процесс со встроенным ручным runner, без второго процесса и cron; отдельный worker требует DATABASE_URL. Это предотвращает совместный доступ процессов к embedded-файлу.

C4 L3: глубокие модули

flowchart LR
  H[HTTP handlers] --> A[Доступ]
  H --> R[Заявки и согласование]
  H --> P[Назначения и ресурсный план]
  H --> V[Отчёты]
  R --> P
  R --> C[Расчёты]
  P --> C
  V --> C
  R --> A
  P --> A
  V --> A
  W[Worker / ручной запуск] --> S[Справочники и синк]
  S --> X[SphereClient / ProjectSource / TimeSource / OneCSource]
  R --> DB[(Транзакционная БД)]
  P --> DB
  V --> DB
  S --> DB
  A --> DB

Модули сами владеют запросами к своим таблицам, а не цепочкой generic repository/service/controller. Общий UnitOfWork — инфраструктурная граница транзакции, не отдельный бизнес-модуль. Доступ обеспечивает policy-функции и scoped query predicates; ни один публичный метод чтения не принимает произвольный SQL scope от клиента.

Сигнатуры ниже — эскиз контракта TypeScript, не код сервиса. Result<T> — дискриминированный результат {ok:true,value:T} либо {ok:false,error:DomainError}; Promise только для I/O. DTO определяются один раз в shared/contracts/, схемы zod выводят типы. ActorContext создаётся сервером через Доступ. ID — номинальные строковые типы; Decimal — точное число, сериализуемое строкой. Tx выдаётся только UnitOfWork; наружу HTTP не выходит.

Заявки и согласование — Requests

interface Requests {
  create(actor: ActorContext, cmd: CreateRequest): Promise<Result<RequestView>>;
  proposeChange(actor: ActorContext, cmd: ChangeAssignment): Promise<Result<RequestView>>;
  decide(actor: ActorContext, cmd: DecisionCommand): Promise<Result<RequestView>>;
  get(actor: ActorContext, id: RequestId): Promise<Result<RequestView>>;
  list(actor: ActorContext, query: RequestQuery): Promise<Result<Page<RequestView>>>;
}
// Команды содержат expectedVersion и commandId; DecisionCommand — approve/return/reject.

Прячет: конечный автомат, validation ролей/ставок, idempotency, проверку версии, снимки первичной заявки, журнал решений и одну транзакцию изменения назначений. Вызывает: Nitro API. Проверяется: L1 автомат и правила решения; L2 через create/decide с настоящей БД; L4 права и ошибки, L5 заявка→план. Requests владеет Request/ResourceLine/AssignmentChange/DecisionLog/CommandReceipt. Вызов Assignments.applyDecision происходит внутри его Tx, а не отдельным HTTP-запросом.

Назначения и ресурсный план — Assignments

interface Assignments {
  previewChange(actor: ActorContext, cmd: ChangeDraft): Promise<Result<ChangePreview>>;
  getPlan(actor: ActorContext, query: PlanQuery): Promise<Result<PlanView>>;
  list(actor: ActorContext, query: AssignmentQuery): Promise<Result<Page<AssignmentView>>>;
  applyDecision(tx: Tx, decision: ApprovedDecision): Promise<Result<AssignmentVersion[]>>;
}

Прячет: дневные интервалы, состояния active/ended, единственное ожидающее изменение, блокировки общей ёмкости людей, версии назначений и согласованную проекцию плана. Вызывает: API для чтения/превью, Requests для решения; ApprovedDecision — внутренний тип модуля, клиент его не создаёт. Проверяется: L1 дневные преобразования; L2 смена версии без двойного учёта и откат; Postgres-проверка конкурентного решения; L4 область РП; L5 замена. Владеет Assignment/AssignmentVersion/AssignmentDay; изменение существующей версии запрещено, текущий указатель меняется транзакционно.

Расчёты — Calculations

interface Calculations {
  employeeNorm(calendar: CalendarMonth, absences: EmployeeMonthAbsences): Decimal | null;
  plannedHours(plan: MonthPlan, availability: EmployeeAvailability): Decimal | null;
  changePlan(plan: AssignmentPlan, change: ChangeDraft, availability: EmployeeAvailability): ChangePreview;
  overloads(plans: AssignmentPlan[], availability: AvailabilityByEmployee): Overload[];
  finance(items: FinancialItem[]): FinanceTotals;
  utilization(periods: PeriodTotals[]): UtilizationTotals;
  releaseDate(plan: AssignmentPlan, availability: EmployeeAvailability): LocalDate | null;
  periodTotal(periods: PersonalPeriod[]): PeriodTotals;
  planTotal(rows: VisibleEmployeePlan[]): PlanTotals;
  utilizationIndicators(input: ScopedUtilizationInput): UtilizationIndicators;
}

Прячет: формулы §1, приоритет days, взвешивание, допуски 0,00001 FTE / 0,001 п.п., отсутствие данных и правила точности. Вызывают: Requests/Assignments/Reports. Проверяется: L1 таблицы AC и property-based fast-check, без БД/сети. База — функции calc.js; глобальные imports seed.js из costRate/viewScope заменяются аргументами. Clock нужен вызывающему модулю для проверки даты, Calculations получает уже определённую дату. Никаких Date.now(), fetch или process.env.

Расчётные входы и полнота

Контракт calculation-contract.md описывает реализованные значения. Для домена будущего сервиса EmployeeAvailability содержит календарь, обе записи отсутствий и их полноту; это не второй тип нормы продажи. PersonalPeriod содержит персональные hours/norm, VisibleEmployeePlan — уже ограниченный состав людей, ScopedUtilizationInput — один scope для P, Pком и A. Null сохраняется до представления. Отсутствия объединяются до расчёта доступных часов; UI не повторяет вычет. Сводные часы без дат распределяются пропорционально календарным часам только как допущение прототипа; рабочий источник и точность дат — Q-06.

Предложение хранения: AbsenceEntry(employeeId, kind=vacation|sick, month, dates или hours, sourceVersion), AbsenceCoverage(employeeId, month, kind, confirmed). Пропуск записи без coverage не является нулём. В прототипе coverage выражено наличием записи в reportData, а не отдельной таблицей. Импорт, версии БД и HTTP-порт отсутствий ещё не реализованы.

Отчёты — Reports

interface Reports {
  get(actor: ActorContext, query: ReportQuery): Promise<Result<ReportView>>;
  exportCsv(actor: ActorContext, query: ExportQuery): Promise<Result<CsvFile>>;
  updateLocalData(actor: ActorContext, cmd: LocalReportDataCommand): Promise<Result<ReportDataVersion>>;
}

Прячет: безопасную область и агрегации, категории проектов, actual coverage, отсутствие факта, текущие кадровые атрибуты, проекцию ставок по viewCost, экранирование CSV. Вызывают API и UI через API. Проверяется: L1 примеры AC 41–66 в Calculations; L2 real DB фильтры/полнота; L4 матрица прав и отсутствие утечек в CSV; L5 отчёт. Владеет только локальными overrides/категориями/ручными actuals/vacations/sickLeaves; импортированные snapshots принадлежат Sync и читаются, не редактируются. Итоги собираются одной согласованной версией снимков, версии возвращаются в метаданных.

Справочники и синк — DirectorySync

interface DirectorySync {
  enqueue(actor: OperatorContext, source: SourceKind): Promise<Result<SyncRunId>>;
  run(input: SyncInput): Promise<Result<SyncResult>>;
  status(actor: ActorContext, source: SourceKind): Promise<Result<SyncStatus>>;
  resolveIdentity(actor: OperatorContext, cmd: IdentityMapping): Promise<Result<MappingVersion>>;
}
// SyncInput: source, runId, asOf, mode=incremental|reconcile; часы/ID внедряются фабрикой.

Прячет: постраничное чтение, staging, checkpoint, дедупликацию, полные сверки, мягкую деактивацию, карантин неподтверждённых сопоставлений и атомарную активацию снимка. Вызывают worker и защищённый операторский API; run доступен программно без scheduler. Проверяется L2 повтор, обрыв, восстановление; L3 адаптеры на фикстурах; L4 только оператор запускает; L5 устаревание. Владеет справочниками, ExternalIdentity, SyncRun/SyncCheckpoint, ImportSnapshot, ActualEntry/Coverage, CalendarVersion/Day, AbsenceSnapshot/Coverage, CostRateVersion. Детерминизм: одинаковые входы+Clock+IdGenerator → одинаковые изменения доменных данных; технический runId может отличаться.

Доступ — Access

interface Access {
  beginLogin(input: LoginStart): Promise<Result<LoginRedirect>>;
  finishLogin(input: LoginCallback): Promise<Result<SessionCookie>>;
  authenticate(input: SessionInput): Promise<Result<ActorContext>>;
  authorize(actor: ActorContext, operation: Operation, object: ObjectScope): Result<Permit>;
  scope(actor: ActorContext, resource: ResourceKind): AuthorizedScope;
  revoke(principalId: PrincipalId): Promise<void>;
}

Прячет: OAuth/OIDC flow, связь external subject→principal, сессии, права и scope, отзыв после деактивации. Вызывают все публичные модули и handlers. Проверяется L1 policy, L2 session/grants, L3 IdentityProvider, L4 полная матрица и production-negative fake-login, L5 вход в тесте. Владеет Principal, Session, AccessGrant; AccessGrant не редактируется Requests. ActorContext нельзя передать через JSON как доверенный объект.

Порты и адаптеры

interface SphereClient { // совместимо по именам и смыслу со задача №215
  getTasks(ids: string[]): Promise<SphereTask[]>;
  listGroupTasks(groupId: string, options: {openOnly: boolean}): Promise<SphereTask[]>;
  searchGroups(q: string): Promise<SphereGroup[]>;
  getGroupMembers(groupId: string): Promise<SphereUser[]>;
  listDepartments(): Promise<SphereDepartment[]>;
  searchUsers(query: {q?: string; departmentId?: string; ids?: string[]}): Promise<SphereUser[]>;
}
interface ProjectSource { read(cursor?: string): Promise<SourcePage<SourceProject>> }
interface TimeSource { read(query: TimeWindow, cursor?: string): Promise<SourcePage<TimeEntry>> }
interface OneCSource {
  calendar(year: number): Promise<SourceSnapshot<CalendarDay>>;
  absences(query: AbsenceWindow, cursor?: string): Promise<SourcePage<SourceAbsence>>;
  costRates(cursor?: string): Promise<SourcePage<SourceCostRate>>;
}
interface IdentityProvider { begin(input: LoginStart): Promise<LoginRedirect>; verify(input: LoginCallback): Promise<VerifiedIdentity> }
interface Clock { now(): Instant; today(zone: 'Europe/Moscow'): LocalDate }
interface IdGenerator { next(): string }
interface CurrentUser { get(): Promise<Result<ActorContext>> }

SphereUser: id/name/position/departmentIds/active; SphereDepartment: id/name/parentId; SphereTask: id/title/stageName/closed/groupId/responsibleId/deadline; SphereGroup: id/name/membersCount. Методы, не нужные стаффингу сейчас, не вызываются ради заполнения данных. Поскольку searchUsers по умолчанию ищет только активных, сверка сохранённых ID выполняется явно через ids с получением ACTIVE=false; отсутствие не доказывает увольнение.

SourcePage содержит items/nextCursor/completeness/sourceVersion; общий транспорт не притворяется, что пагинация у всех систем одинаковая. ProjectSource реализуется GroupProjectSource, DealProjectSource или SmartProcessProjectSource, без проверки типа сущности внутри домена. TimeSource и OneCSource — проектируемые контракты: фактические endpoint и схема ещё не известны. Для каждого — HTTP-адаптер и Fake-адаптер. В e2e фейковые HTTP-серверы проверяют ещё и транспорт; модульные тесты используют in-memory fake порты.

Модель хранения и транзакции

erDiagram
  REQUEST ||--|{ RESOURCE_LINE : contains
  REQUEST ||--o{ DECISION_LOG : records
  RESOURCE_LINE ||--o{ ASSIGNMENT : creates
  ASSIGNMENT ||--|{ ASSIGNMENT_VERSION : versions
  ASSIGNMENT ||--o{ ASSIGNMENT_CHANGE : changes
  EMPLOYEE ||--o{ ASSIGNMENT : assigned
  PROJECT ||--o{ REQUEST : requested
  PROJECT ||--o{ PROJECT_MANAGER : grants
  EMPLOYEE ||--o{ ACTUAL_ENTRY : worked
  PROJECT ||--o{ ACTUAL_ENTRY : measured
  CALENDAR_VERSION ||--|{ CALENDAR_DAY : defines
  EMPLOYEE ||--o{ ABSENCE_ENTRY : absent
  ABSENCE_SNAPSHOT ||--o{ ABSENCE_ENTRY : contains
  ABSENCE_SNAPSHOT ||--o{ ABSENCE_COVERAGE : confirms

Таблицы нормализованы: нет одной JSON-записи State/main, которая блокирует весь сервис. JSON допустим для неизменяемого before/after в DecisionLog и валидированного сырого staging с ограниченным сроком хранения; фильтруемые поля и внешние ключи — колонки.

API и отличие от прототипа

/api/v1 — предложение нового контракта, а не обещание бинарной совместимости с прототипом. Старые action=create/approve/return/reject/change сохраняют бизнес-смысл; manager из строки заменён проверенной личностью, lineIndex — стабильным ID, global revision — версиями агрегатов. Старый GET State не переносится в рабочий API, чтобы не отдавать данные всех проектов. Если нужен импорт прототипа, он делается отдельной проверяемой операцией (Q-10).

Метод / путь Модуль / контракт Версии и ответ
GET /api/v1/requests; GET /api/v1/requests/:id Requests.list/get 200 scoped Page/RequestView
POST /api/v1/requests Requests.create 201 RequestView; commandId; projectId/lines/comment
POST /api/v1/requests/:id/decisions Requests.decide 200; expectedVersion, commandId, action, lines только для первичного approve, comment, allowLoss, expectedRateVersions
GET /api/v1/assignments; GET /api/v1/plan Assignments.list/getPlan 200; period/filter/cursor
POST /api/v1/assignments/:id/preview Assignments.previewChange 200; изменение без записи
POST /api/v1/assignments/:id/changes Requests.proposeChange 201; expectedAssignmentVersion, mode/date/FTE/employeeId/reason
GET /api/v1/reports/:kind; GET /api/v1/reports/:kind.csv Reports 200 JSON/CSV; только известные kind, одна область
PATCH /api/v1/report-data Reports.updateLocalData 200; expectedVersion, commandId; только локальные поля
GET /api/v1/sync/status; POST /api/v1/sync/:source DirectorySync 200 статус для РМ; 202 runId оператору запуска
GET /auth/sphere; GET /auth/sphere/callback; POST /auth/logout Access redirect/cookie; logout отозвает сессию
GET /api/health/live; GET /api/health/ready Инфраструктура 200 live; ready 503 при недоступной БД/несовместимой схеме

Общая ошибка {error:{code,message,details?},requestId}: 400 INVALID_REQUEST; 401 UNAUTHORIZED; 403 FORBIDDEN; 404 NOT_FOUND для чужого объекта; 409 VERSION_CONFLICT/CHANGE_PENDING/RATE_CHANGED/COMMAND_KEY_REUSED; 422 OVERLOAD/LOSS_CONFIRMATION_REQUIRED/INVALID_TRANSITION/UNMAPPED_REFERENCE; 503 SOURCE_UNAVAILABLE/STORAGE_UNAVAILABLE. Тексты локализуются, тесты проверяют коды. Область прав применяется до агрегации и пагинации; стабильная сортировка (createdAt,id) или (name,id), cursor, ограниченный размер страниц. Подробности ошибки не раскрывают скрытые ставки.

Правила проекта и зачем

Правило Зачем
TypeScript strict, Node 22 LTS, pnpm и lockfile; Nuxt 4 app/ + server/; @nuxt/eslint Единый предсказуемый инструментальный контур со задача №215; версии патчей фиксируются при старте реализации.
domain/ — чистый TS; handlers = zod → module → HTTP Формулы и решения проверяются без Nuxt/браузера.
shared/contracts/ с выводом TS из zod, серверные модели отдельно Не допускает ручного расхождения DTO клиента и сервера и утечки служебных полей.
Компоненты отображают; composables управляют UI; глобальный state только с описанным владельцем Состояние формы не перемешивается с кешем всей компании.
Postgres + Drizzle; миграции в репо, без ручного DDL Любой разработчик воспроизводит схему и историю обновлений.
Нет DATABASE_URL → PGlite; каждый тест начинает с чистой схемы миграций Локальная работа без Docker/внешней БД; production readiness требует Postgres.
DB выбирается composition root; деньги decimal/копейки, не float Домен не зависит от драйвера и не теряет точность финансов.
Bitrix24, Time, 1С, Clock, IdGenerator, CurrentUser — внедряемые порты Воспроизводимые тесты и замена внешнего источника без переписывания правил.
Нет fetch/Date.now/process.env в домене Побочные эффекты видны на границах и подменяются.
Builders и сценарные seed на основе seed.js — один источник для L1–L5 Числовые примеры одинаковы в тестах, UI и стенде.
Тестовый вход компилируется только при NODE_ENV=test; prod негативный тест обязателен Флаг fake-интеграций не открывает вход под чужой ролью.
Синк имеет явный run; scheduler только инициирует; повтор идемпотентен Оператор и тест могут повторить сбой без ожидания cron.
Fake HTTP-серверы источников запускаются со стендом; без боевых credentials Демонстрация и e2e не зависят от рабочих систем.
Секреты — только private runtimeConfig/env, .env.example содержит лишь имена Сборка клиента и репозиторий не раскрывают доступ.
Стабильная сортировка и типизированные ошибки Пагинация и автоматические проверки воспроизводимы.
Структурные логи + Sentry + health + UI последнего синка Оператор видит источник и стадию сбоя, не читая весь код.
ID требований в именах тестов; scripts/req-coverage; покрытие домена по testing.md Потерянные бизнес-требования обнаруживаются в CI.

Будущая структура: app/{components,composables,pages}, shared/contracts, server/{api,auth,composition}, domain/{requests,assignments,calculations,reports,access,sync}, adapters/{sphere,time,onec,identity,db}, worker, db/migrations, tests/{domain,module,contract,api,e2e,fixtures,builders}. Это ориентир ответственности, не scaffolding в этой задаче. SQL и adapters конкретного модуля можно держать рядом с ним; не создавать интерфейс на каждый вызов ORM.

Эксплуатация и NFR 03

Логи: requestId/runId, source, duration, resultCode, checkpoint; без токенов, webhook URL, ставок и полных ответов API. Метрики: sync_last_success_age, sync_errors, pending_jobs, quarantined_records, API latency/error rate, backup/restore age. Alert по порогам Q-09; source outage не делает live=false и не уничтожает кеш. UI показывает stale с временем и отдельную ошибку, а не зелёный статус по наличию старого кеша.

Операторский порядок: проверить health/schema → source status и runId → исправить секрет/маппинг → явный reconcile → проверить изменённые/карантинные строки. Cursor не сбрасывать вручную в SQL. Резервирование Postgres и ключей отдельно, шифрованные backup, восстановление в изолированную БД с проверкой FK/версий/журнала и smoke. Частота, RPO/RTO, retention, удаление и нагрузка остаются Q-09. Откат релиза только на совместимую схему; миграции expand/contract, перед необратимой миграцией проверенная копия.

Проверенные технические источники

На 13.10.2026 Nitro tasks помечены experimental — это аргумент ADR-0002, а не запрет будущего перехода. Drizzle PGlite подтверждает отдельный драйвер. PGlite socket описывает single-connection ограничение: блокировки конкурентных транзакций отдельно проверяем на Postgres. Конкретные версии Nuxt/Nitro/Drizzle нужно закрепить lockfile и smoke в П1; текущая документация Nitro может относиться к другой основной версии, конфиг отсюда дословно не копируется.

service/docs/calculation-contract.md

Расчётный контракт дополнения

Редакция 13.10.2026. Фактический эталон — prototype-v2 commit 5213150; это контракт демо и принятых формул, а серверная архитектура остаётся предложением. Все NEW-01–09 реализованы пакетами; общая QA и риск-ревью ещё не завершены. Спецификация, прослеживаемость, словарь.

Единый владелец и границы

В прототипе формулы принадлежат js/calc.js, валидация/запись — js/api.js, данные — js/seed.js, экраны только вызывают и форматируют их. API синхронный mock api.post(action,payload) с localStorage и глобальной revision, а не реализованный HTTP-сервис. Ошибка 400 не записывает состояние, конфликт revision — 409, недоступное/повреждённое хранилище — 503. Сохранение reportData заменяет объект целиком; форма переносит неизменённые поля. Серверные стабильные ID, Decimal, авторизация, журналы/версии БД, 1С/Time/Сфера пока проектируются. Переключатель РМ/РП демонстрирует scope, не доступ.

Дополнение имеет приоритет над противоречащими формулами v1.7. P=FTE×персональная H принят; отдельной нормы продажи нет. Новые процессы замещения, переноса работ, пересогласования денег и политика исторического пересчёта рабочего сервиса не добавлены. Исходные документы и изображения v1.7 сохранены как история.

Календарь и доступные периоды

Hкал = сумма date→hours федеральной пятидневки 40 ч. Праздники и перенесённые выходные имеют 0 ч, сокращённые рабочие дни — 7 ч, обычные — 8 ч. 20.02.2027 — рабочая суббота 7 ч; 22.02 — перенесённый выходной. Региональные исключения в демо не включены.

Год янв фев мар апр май июн июл авг сен окт ноя дек год
2026 120 152 168 175 151 167 184 168 176 176 159 176 1972
2027 120 151 176 175 152 167 176 176 176 168 159 176 1972

Источники, проверенные при подготовке календаря 13.10.2026: 2026, 2027, постановление №1187. Это опубликованные данные, не проверка API 1С.

API принимает месяцы 2026-01…2027-12; иной месяц — 400. Форма заявки предлагает до 12 месяцев и уменьшает выбор у конца календаря, API — до 24 уникальных месяцев. Первый месяц шестимесячного плана ≤2027-07; некоммерческого отчёта с периодом 3/6/12 ≤2027-01. Недельное окно — 12 недель: неделя, включающая 2025, целиком неизвестна; норму 2025 не подставляют. Январь/февраль/март 2026 имеют 8/4/0 таких недель. Календарные даты кадрового начала могут быть старше 2026; это не добавляет часы отсутствующего календаря.

Отпуск, больничный, пересечения и полнота

reportData.vacations и reportData.sickLeaves содержат уникальные записи сотрудник×месяц. Каждая запись — одно из:

{"employee":"e1","month":"2026-10","hours":16}
{"employee":"e1","month":"2027-02","dates":["2027-02-20","2027-02-22"]}

Hours — подтверждённые рабочие часы 0…Hкал, уже без пересечений и нерабочего времени. Dates — полные дни этого месяца, не интервалы частичных часов. Повтор даты внутри записи нормализуется, неверная дата/месяц отклоняется. Объединение отпускных и больничных дат исключает повтор; праздник/выходной в dates даёт 0 ч. Пример: отпуск 20.02+22.02.2027, больничный 20.02 → исключено 7 ч, H=144.

Для пересечений оба вида вводят dates. Положительную сводку hours и датированные отсутствия одного сотрудника/месяца смешивать нельзя; API просит оба вида указать датами. Сводные часы — допущение демо: доступность каждого рабочего дня уменьшается пропорционально его календарным часам. Без дат реальное положение отсутствия в месяце неизвестно; это не прогноз больничных и не доказательство точности кадровой интеграции. Для точного дневного плана нужны dates.

Наличие записи каждого вида подтверждает полноту: пустое поле удаляет запись → unknown; hours=0 либо dates=[] подтверждает отсутствие. H неизвестна, если нет хотя бы одной из двух записей. Полная сумма отсутствий не может превышать Hкал; отказ атомарный. H = Hкал − объединённые рабочие часы отсутствий; N = сумма H выбранных сотрудников/месяцев. Hкал остаётся известной и показывается отдельно. Повторно отпуск/больничный из H/N не вычитаются; колонок с дополнительным вычетом нет.

P, фактические часы и деньги

Обычный месячный план: P = FTE × H. Для дневного плана: P = сумма dailyFte(date) × availableDayHours(employee,date). days[date] имеет приоритет, явный 0 сохраняется. Если H неизвестна и загрузка ненулевая, зависимые P, деньги и проценты неизвестны. Известная нулевая загрузка даёт 0 часов даже при неизвестной H. Известная H=0 даёт 0 плановых часов; P/H и проценты с нулевой нормой — прочерк.

До выбора сотрудника строка «На подбор» использует Hкал и известную R, C неизвестна; после выбора — H сотрудника. Эта граница явно отражает отсутствие персональной нормы до назначения. Мок допускает создание заявки при неизвестной H, но требует подтверждённые обе записи отсутствий для всех назначенных месяцев при согласовании. Проверка перегрузки — сумма дневных FTE в рабочие дни с доступными часами, включая рабочую субботу; подтверждённый день с 0 доступных часов исключается. Неизвестность не снимает проверку. До решения change план неизменён; исходные месячные ставки сохраняются при дневном изменении. При выводе 0,5 FTE с 16.11.2026 и нулевых отсутствиях → 35,5 ч, месячный FTE=35,5/159.

actuals — запись сотрудник×проект×месяц: одно из fte (0…2) или hours. Введённый FTE переводится в A через H; явные hours сохраняются напрямую и видны даже при неизвестной H. Hours ограничены 2Hкал и при известной H также 2H; это граница ручного ввода, а разрешение импорта другого факта не принято. Пусто удаляет запись, 0 сохраняется. Отсутствие записи хотя бы для одного выбранного сотрудника×проекта×месяца делает сумму A неполной; известная детализация не пропадает. Если проектов выбранного типа нет, A=0. Отпуск/больничный не являются работами.

I=P×R, расход=P×C, разница=I−расход. Часы/деньги не округляются до суммы; отображление округляет. Финансы списка, формы, карточки, месячных строк и отчёта используют одни исходные часы/ставки; неизвестные значения в CSV пусты, не 0. Изменение введённых отсутствий меняет текущие P и деньги прототипа, включая уже согласованные заявки; нового процесса пересогласования нет. Legacy месячные R сохраняются, показанная средняя взвешена P.

Форма/карточка: отрицательная разница красная, неотрицательная зелёная. Месячный и списочный расход красный только при отрицательной разнице; неизвестный без цвета. Пример Анны: Hкал=176,V=16,B=0 → H=160, FTE=0,5 → 80 ч, R/C=3500/2800 → 280000/224000 ₽. B=8 → H=152, P=76, 266000/212800 ₽. Пример потери при 88 ч и R/C=4000/4300 → 352000/378400/−26400 ₽.

Σ, Итог и показатели отчётов

Σ ч = сумма часов; Σ FTE·мес = сумма месячных FTE; Σ % = сумма часов / сумма персональных норм ×100. Для 0,5/1/0,75 при нормах 160/176/176: 2,25 FTE·мес, 388 ч, процент 388/512×100. Это не сумма процентов и не средняя FTE.

Нижний Итог использует видимых сотрудников ровно один раз. Проекты — расшифровка, не дополнительный вклад. Свёртка не меняет его; поиск/свободный ресурс/область РП меняют состав. Поиск проекта оставляет полную загрузку найденных сотрудников: итог равен показанным сотрудникам, как существующее поведение плана. Месячный FTE группы — сумма персональных FTE, процент группы — сумма часов / сумма H.

Три показателя утилизации используют один scope: P/N, Pком/N, A/N. Коммерческий числитель выбирается по категории «Коммерческий» ИЛИ типу commercial (как utilizationIndicators), категории таблиц выбирает projectType с приоритетом подробной категории. Неизвестная/нулевая N даёт прочерк с причиной, неполный A — пояснение. Категории не суммируют свой общий знаменатель. Накопительный процент — суммы часов / суммы H при одном составе людей; не среднее процентов.

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

Четыре верхние вкладки: План, Заявки, Назначения, Отчёты. В Отчётах пять разделов: Утилизация, Финансы, Распределение, Некоммерческие проекты, Статистика. ЦФО/подразделение в Распределении и CSV берутся из введённых данных, пропуск — прочерк.

Контрольное демо и проверка

Все 24 месяца отсутствий демо подтверждены; отпуск Анны октября 16 ч, прочие 0, больничные 0. Октябрь: N=1040,P=432,Pком=256, FTE группы=2,5; факт неполный. Ноябрь: N=954,P=A=477, полный факт →50%. Норма 6 сотрудников ноября =6×159, без выдуманных отсутствий. Старое localStorage не переписывается: новые отсутствующие sickLeaves неизвестны, для контрольного демо нужен сброс.

Заявка Доход ₽ Расход ₽
demo-1 2446550 2388700
demo-2 1548700 1246200
demo-3 2176900 неизвестен

Детальные проверки и файлы — матрица покрытия. Числовая сверка документа выполняется на неизменном 5213150; она не заменяет будущую общую QA.

service/docs/data-flows.md

Потоки данных и контракты интеграций

Редакция 13.10.2026 по дополнению NEW-01–09 и реализации прототипа 5213150. Расчётное поведение ниже принято для прототипа; инфраструктура, авторизация, интеграции и хранение рабочего сервиса остаются предложениями. Общая QA всех NEW ещё предстоит. Расчётный контракт, матрица покрытия.

Статус: предложение; INT 01–04, NFR 01–03. Все частоты ниже — настраиваемые стартовые рекомендации, не утверждённый SLA. Сфера/Time только читаются. Реальные endpoint Time и конфигурация 1С неизвестны: в таблицах честно указан блокирующий контракт, а не выдуманные методы. Публичные REST-методы Bitrix24 проверены по документации; доступность полей конкретной коробки Сферы ещё требует read-only контрактной проверки.

Общий протокол входящих потоков

  1. Scheduler или оператор создаёт SyncRun. Уникальный source+scope+scheduledSlot не даёт дубликат задания; worker берёт lease с TTL и heartbeat. run получает фиксированный asOf и snapshot конфигурации.
  2. Адаптер читает страницы в staging, следует next/cursor конкретного API; не считает короткую страницу концом, если есть next. Сортировка по стабильному ID; cursor и исходные version/hash сохраняются. Bitrix может вернуть ошибку в HTTP 200: проверяется тело error.
  3. Сетевой timeout: предложение 20 с, не более 5 попыток на run, exponential backoff с jitter 1–60 с и учётом Retry-After. 429/5xx — повтор; 401/403 — остановка до исправления credentials; невалидная схема — quarantine и failed. Повтор/задержка инъецируются для детерминированных тестов.
  4. Только полный валидный снимок/страница с подтверждённой границей коммитится с checkpoint в одной транзакции. Для full reconcile удаление отсутствующих допускается только после успешного завершения всей области; обрыв страницы не меняет статус ранее известных записей. Незнакомые ID не сопоставляются по ФИО автоматически.
  5. Lease имеет fencing version: потерявший lease worker не может активировать старый snapshot. Повтор обработки — upsert по внешнему ключу и версии/hash. Snapshot активируется атомарно; отчёт не смешивает старые и новые страницы одного периода.
  6. При недоступном источнике сохраняется последний целый снимок, lastSuccessAt не меняется, UI показывает stale. Freshness доступа и ставок строже, чем справочных имён (Q-03/Q-08); ошибка синка не даёт дополнительных прав.

F1. Сотрудники, подразделения, ресурсные роли — INT 02

sequenceDiagram
  participant W as Sync runner
  participant B as SphereClient
  participant S as Сфера
  participant D as БД
  W->>B: listDepartments / searchUsers
  B->>S: department.get / user.get, страницы
  S-->>B: ID, атрибуты, ACTIVE, next
  B-->>W: Нормализованные записи и полнота
  W->>D: staging + проверка mapping ролей
  W->>D: activate snapshot, мягкая деактивация, отзыв сессий
  Note over W,D: История назначений не удаляется
Источник → получатель Сущность и поля / метод Триггер Частота Ключ и идемпотентность Ошибка / недоступность Удаление / деактивация
Сфера → DirectorySync → Employee user.get: ID, NAME, LAST_NAME, ACTIVE, WORK_POSITION, UF_DEPARTMENT; активные для кандидатов, сохранённые ID отдельно без фильтра активности Расписание + ручной run 15 мин; полная сверка известных ID раз в сутки sphere:user:ID; upsert sourceVersion/hash, имена не ключи Общий протокол; отсутствующий обязательный ACTIVE/ID — неполный снимок, не default active Явный ACTIVE=false отключает вход и выбор в новых строках, отзывает сессии; отсутствие ID → missing_pending, запрет новых назначений, отдельная проверка, историю оставить
Сфера → DirectorySync → Department department.get: ID, NAME, PARENT; user.get.UF_DEPARTMENT задаёт связи many-to-many Расписание + ручной run 15 мин; полная сверка сутки sphere:department:ID; уникальные employeeId+departmentId Цикл дерева/неизвестный parent → quarantine; последний валидный граф видим как stale Подтверждённо отсутствующее подразделение inactive; связи/история сохраняются; отчёт показывает «Не указано», если основное не выбрано
Сфера + утверждённое mapping → ResourceRole WORK_POSITION и/или согласованное UF-поле; user.fields для проверки схемы; не существует универсального REST «роль стаффинга» Изменение профиля или версии mapping, явный пересчёт При синке 15 мин; mapping по запросу РМ employeeId+mappingVersion; результат пересчёта не создаёт вторую роль Неизвестная должность → unmapped и запрет подбора до решения, не автоматическая выдача доступа Роль не удаляет прошлые назначения; новая роль действует для новых решений; права pm/rm отдельно

задача №215 использует подбор роли по WORK_POSITION как подсказку. Здесь нельзя автоматически переносить его семь ролей в шесть ролей ТЗ. Mapping и многоподраздельность требуют Q-07. Для аккаунта, внезапно исчезнувшего из выборки, доступ временно приостанавливается, но увольнение не фиксируется без подтверждения. Это отличается от необратимого удаления данных.

F2. Проекты и РП — INT 01

sequenceDiagram
  participant W as Sync runner
  participant P as ProjectSource
  participant B as Сфера
  participant D as БД
  W->>P: read(cursor)
  P->>B: sonet_group.get ИЛИ crm.deal.list ИЛИ crm.item.list
  B-->>P: ID, название, ответственный, закрытие
  P-->>W: SourceProject и reference пользователя
  W->>D: ExternalIdentity + Project + ProjectManager
  Note over W,D: Нет подтверждённого mapping РП — нет доступа к проекту
Источник → получатель Сущность и поля / метод Триггер Частота Ключ и идемпотентность Ошибка / недоступность Удаление / деактивация
Сфера: рабочая группа → ProjectSource → Project sonet_group.get: ID, NAME, OWNER_ID, CLOSED, PROJECT; sonet_group.user.get: USER_ID, ROLE для состава. OWNER_ID только кандидат РП, не бизнес-гарантия Расписание + ручная сверка 15 мин; полная сутки sphere:group:ID; membership по projectId+principalId; версия mapping Если владелец/РП не сопоставлен, quarantine прав; старые имена можно читать, новые заявки блокировать CLOSED=Y закрывает новые заявки, назначения остаются; отсутствие после полной сверки → missing_pending, не hard-delete
Сфера: сделка → альтернативный ProjectSource crm.deal.list, crm.deal.fields: ID, TITLE, ASSIGNED_BY_ID, STAGE_ID, CLOSED, согласованное UF-поле РП; не любая сделка — проект Расписание + ручная сверка выбранной в Q-01 области 15 мин; полная сутки sphere:deal:ID, sourceVersion/hash CRM-права могут скрывать сделки: пропуск не означает удаление; failed/stale и сверка с владельцем подключения Закрытие сделки трактуется по утверждённому mapping статусов проекта; история сохранена
Сфера: смарт-процесс → альтернативный ProjectSource crm.type.list → entityTypeId; crm.item.fields и crm.item.list: id, title, assignedById, stageId, согласованные custom fields Расписание + ручная сверка после настройки типа 15 мин; полная сутки sphere:smart:entityTypeId:id; без типа ID неоднозначен Неизвестные поля/стадии не дают доступ автоматически; схема блокирует активацию снимка Терминальная стадия из mapping закрывает проект; отсутствие после полного обхода — pending confirmation

Рекомендуем рабочие группы: задачи Time уже связаны со Сферой, tasks.task.list возвращает groupId, подход согласуется со задача №215 и не требует CRM-модели для планирования. Сделка уместна, если именно там уже ведутся проекты и РП. Смарт-процесс уместен при действующем реестре проектов с отдельным жизненным циклом; создавать его только ради интеграции не требуется. Q-01 решает сущность, признак проекта, связь РП (один/несколько) и закрытие. Любой вариант нормализуется в один Project; остальной домен не меняется.

F3. Фактические часы из Time Лоции

sequenceDiagram
  participant W as Sync runner
  participant T as TimeSource
  participant B as SphereClient
  participant D as БД
  W->>T: read(window, cursor)
  T-->>W: entryId, userId, taskId, date, seconds, version
  W->>B: getTasks(неизвестные taskId)
  B-->>W: taskId и groupId
  W->>D: сопоставление сотрудника и проекта, quarantine неизвестных
  W->>D: upsert entries + coverage + checkpoint
  Note over W,D: Только полное покрытие позволяет вывести явный ноль
Источник → получатель Сущность и поля / метод Триггер Частота Ключ и идемпотентность Ошибка / недоступность Удаление / деактивация
Time → TimeSource → ActualEntry/Coverage Реальный read API/export не предоставлен, Q-05. Требуем поля entryId, sphereUserId, sphereTaskId, workDate, durationSeconds, updatedAt/version, deleted, границы полного покрытия; TimeSource.read — наш порт, не метод Time Расписание, ручная сверка периода Предложение 15 мин текущий и предыдущий месяц; ночная сверка открытых периодов; закрытые по запросу time:entry:entryId; новая версия заменяет старую, не прибавляется; при экспорте без ID — атомарная замена полного окна, не hash как ID Нет схемы/права/coverage → unknown/stale; карантин сохраняет исходные строки, частичная сумма не становится полной Tombstone удаляет вклад записи новой версией; отсутствие считается удалением только в полном снимке окна; старый факт хранится в истории
Сфера → SphereClient → TaskProjectMapping tasks.task.list с ID-пачками до 50 как задача №215: id, groupId, responsibleId; пользователь факта берётся из Time, не из responsibleId По новым taskId при импорте; перепроверка mapping В каждом run для неизвестных; полная по окну сутки sphere:task:ID → projectId, version/effectiveFrom Несопоставленный task/group или проект иной сущности → quarantine; ручная подтверждённая связь по ID (Q-01/Q-05) Удаление задачи не удаляет списанные часы; перенос в группу не переписывает исторический проект без аудируемого решения

Сумма seconds/3600 даёт A; совместимое actualFte = A/H при H>0. Исходные секунды сохраняются, перерасчёт календаря не меняет A. Агрегация employeeId×projectId×month. Нет записи ≠ 0; нули выводятся только когда источник подтверждает полное покрытие соответствующей области. Нельзя обрезать импортированный факт выше 2 H: это конфликт с FR 64, запись уходит в quarantine и Q-05, пока правило не утверждено. Отпуска не извлекаются из нерабочих задач эвристикой.

Ручной факт прототипа не суммируется с Time автоматически: предложение — Time имеет приоритет в покрытой области, ручной fallback только вне неё, correction — отдельная аудируемая запись после утверждения Q-05. Часы отпуска пока отдельные локальные записи РМ; подключение отсутствий из 1С требует отдельного подтверждённого контракта (Q-06).

F4. Календарь, отпуск, больничный и расходные ставки

sequenceDiagram
  participant W as Sync runner
  participant C as Calendar / Absence / Rate sources
  participant D as БД снимков и полноты
  participant R as Calculations / Requests
  W->>C: calendar(year), absences(window), costRates
  C-->>W: Версии, рабочие часы дней, отсутствия и coverage
  W->>D: validate, stage, activate snapshot
  R->>D: Календарь, обе записи отсутствий, C и версии
  R->>R: Объединить даты, вычислить H и P, N как сумму H
  R->>D: Решение и costRateSnapshot в одной транзакции
  Note over D,R: Unknown не равен 0, повторного вычета отсутствий нет

Это схема предлагаемого рабочего подключения. В прототипе источники — опубликованный календарь в calc.js и ручные reportData в localStorage. Никакой sync runner, БД или 1С не запускались. Контракт отсутствий источника остаётся неизвестным, новый процесс пересогласования не вводится.

Источник → получатель Сущность и поля / метод Триггер Частота Ключ и идемпотентность Ошибка / недоступность Удаление / деактивация
Опубликованный календарь → calc.js (демо); будущая 1С/утверждённый источник → CalendarVersion/Day Демонстрационный календарь 2026–2027, date→0/7/8 часов, праздники/переносы/рабочая суббота. Будущий endpoint неизвестен (Q-06); нужны calendarId, date, hours, version, timezone В демо статические данные; в сервисе предложение расписания/ручной публикации Предложение сутки на текущий/следующий год calendarId+version+date; версия целиком Неверные/пропущенные дни — отказ версии; за границами демо месяца — 400, недельная неделя до календаря неизвестна Версию не заменять отсутствующим днём как будто это выходной; политика истории вне дополнения
РМ → api.post report-data (демо); будущий Reports/AbsenceSource → AbsenceEntry/Coverage employee/month, vacations и sickLeaves; одно из hours либо dates полных дней. В будущем источник, kind, sourceVersion и coverage; endpoint 1С неизвестен Ручное сохранение; импорт только после получения контракта В демо по запросу; частота рабочего источника не утверждена employee+month+kind; записи уникальны; даты объединяются Пусто → неизвестная H; 0/пустой dates подтверждают отсутствие. Неверные даты, смешанная положительная сводка с датами, сумма >Hкал — 400 атомарно Удаление записи возвращает unknown; история/деактивация рабочего контура остаются проектируемыми
Будущая 1С/утверждённый источник → CostRateVersion Endpoint неизвестен (Q-04); employeeId, validFrom/To, costPerHour, currency, version и проверенное mapping Предложение refresh при stale перед решением Предложение сутки; возраст Q-03/Q-04 employeeId+validFrom+sourceVersion Нет C/валюты/mapping → unknown, рабочее approve без ставки запрещено Снимок согласованной C не переписывается текущим справочником; новая политика ставок не вводится

Hкал = сумма календарных часов. H сотрудника = Hкал − объединённые рабочие часы отсутствий; P=FTE×H либо сумма дневных FTE×доступные часы дня. N=ΣH. Отпуск/больничный из N больше не вычитаются. Обе записи обязательны для известной H; календарная норма и явные фактические часы видны при неполноте. Сводные часы уже очищены от пересечений и пропорционально распределены в дневном плане; точные пересечения требуют дат обоих видов. Это допущение демо, не утверждённый формат 1С. См. полный контракт.

Источники календаря демо: 2026, 2027, переносы 2027, №1187. Сверка источников выполнена при реализации календаря 13.10.2026; региональные исключения не включены.

F5. Вход и определение роли — NFR 01

sequenceDiagram
  participant U as Браузер
  participant A as Access
  participant I as Bitrix24 OAuth или SSO
  participant D as Session и Grants
  U->>A: начать вход
  A-->>U: redirect + одноразовый state
  U->>I: аутентификация
  I-->>A: callback с code и state
  A->>I: серверный обмен и проверка личности
  I-->>A: подтверждённый subject
  A->>D: найти Principal, active, AccessGrant
  A-->>U: HttpOnly cookie или отказ доступа
  U->>A: запрос бизнес API
  A->>D: проверить сессию, активность и scope
Источник → получатель Сущность и поля / метод Триггер Частота Ключ и идемпотентность Ошибка / недоступность Удаление / деактивация
Bitrix24 OAuth → Access → Session Authorize/token endpoints конкретного портала/приложения подтверждаются Q-08; после обмена user.current: ID/ACTIVE. Code flow, state, фиксированный redirectUri; секреты только server Начало входа и callback Каждый вход; refresh по expires_in при необходимости issuer/portal+userId → Principal; state одноразовый, повтор callback не создаёт чужую сессию Неверный state/issuer/redirect → отказ; OAuth недоступен → новый вход запрещён; токены/коды не логируются ACTIVE=false → revoke sessions; локально отозванный grant проверяется на каждом API; скрытая деактивация ограничена freshness Q-08
Корпоративный SSO → альтернативный IdentityProvider OIDC discovery/authorize/token/userinfo по metadata согласованного issuer; sub/iss/aud/exp/nonce, PKCE S256; связи sub→sphereUserId только подтверждённые Вход, обновление сессии Каждый вход, сроки Q-08 issuer+sub; не email, одноразовый state/nonce Проверка подписи/JWKS/audience/expiry; ошибка → deny, не fallback на самозаявленный ID Событие revoke при наличии и периодическая сверка; удаление связи закрывает сессии, Employee остаётся
Серверный grant mapping → Access → scoped API AccessGrant principalId, role pm/rm, projectIds, viewCost; источник состава РП — утверждённый F2, РМ — назначение уполномоченным владельцем Каждый API-запрос; смена grants Без кеша права в первом релизе, источник проверяется по freshness principalId+grantVersion; клиент не задаёт роль Нет grant → 403; чужой объект → 404; неизвестная свежесть доступа после согласованного предела → deny Отзыв применяется к уже открытой сессии на следующем запросе, история автора сохранена

Сессия — случайный opaque ID в Secure/HttpOnly/SameSite=Lax cookie, запись/expiry на сервере; state для OAuth хранится отдельно с коротким TTL. POST/PATCH требуют CSRF token и проверку Origin. Нельзя считать локальное приложение Bitrix с произвольным DOMAIN в callback доверенным источником: issuer/домен жёстко allowlisted. В варианте Bitrix поддержка PKCE проверяется на портале; до подтверждения не объявляется готовой. Служебный webhook для синка не идентифицирует посетителя.

F6. Заявки, назначения и мастер-данные — INT 03, NFR 02

sequenceDiagram
  participant U as РП или РМ
  participant A as Requests
  participant P as Assignments
  participant D as БД
  U->>A: commandId, expectedVersion, действие
  A->>A: доступ, валидация, idempotency
  A->>D: BEGIN и блокировки сотрудников
  A->>D: перечитать заявку, версии, ставки
  A->>P: applyDecision в той же Tx
  P->>P: весь дневной план, лимит FTE
  P->>D: новая версия назначения
  A->>D: DecisionLog + Request + CommandReceipt
  A->>D: COMMIT
  A-->>U: результат или типизированный конфликт
  Note over A,D: Любая ошибка до COMMIT — полный ROLLBACK
Источник → получатель Сущность и поля / метод Триггер Частота Ключ и идемпотентность Ошибка / недоступность Удаление / деактивация
РП/РМ → Requests → локальная БД /api/v1/requests, /decisions, /assignments/:id/changes; Request/ResourceLine/Assignment/Change/DecisionLog; методы и поля — architecture.md Явное действие с серверной авторизацией По запросу commandId+principalId и hash; версии агрегатов, уникальное pending change Конфликт/недоступная БД → ошибка без частичной записи; форма сохранена, повтор безопасен Физического удаления решения нет; вывод — новая версия дневного плана, сотрудник/проект inactive сохраняет историю
Сервис → Сфера Нет потока записи в рекомендуемом варианте; INT 03 ожидает Q-02 Не запускается до решения заказчика Отключён Локальные ID — мастер; внешние ID только справочников Недоступная Сфера не откатывает уже совершённое локальное решение; stale-политика для новых действий отдельно Никакие delete/update Bitrix не вызываются

Если заказчик выбирает Bitrix мастер: заменить порт хранения Requests/Assignments, заново спроектировать атомарность решений и журнал/компенсации, пересмотреть ADR-0001 и приёмку. Это существенная развилка, не «флажок». Если нужна только публикация копии решения — отдельный outbox после локального commit, ключ eventId/version, retry и dead-letter; поле локального состояния не объявлять синхронизированным до подтверждения. В текущие пакеты запись обратно не включена.

Достоверность API и источники

Проверено 13.10.2026 по первичной документации: рабочие группы и поля, пользователь текущего токена, ограничения user scope, department API и REST 3.0, crm.item.list, поля CRM. REST v2 department.get выбран для совместимости задача №215; новые humanresources методы не обязательны для этой архитектуры.

Возможность вызова конкретного метода не доказывает наличие прав на все записи. Перед живым импортом L3 smoke на тестовом портале проверяет scope, типы ID/ACTIVE, next, доступ к неактивным, группе/проекту и полям. Боевую Сферу в этой задаче не вызывали; тестовый задача №27 не нужен для документирования неподтверждённых контрактов.

service/docs/open-questions.md

Решения заказчика

Редакция 13.10.2026 по дополнению NEW-01–09 и реализации прототипа 5213150. Расчётное поведение ниже принято для прототипа; инфраструктура, авторизация, интеграции и хранение рабочего сервиса остаются предложениями. Общая QA всех NEW ещё предстоит. Расчётный контракт, матрица покрытия.

Статус вопросов рабочего подключения ниже — не решено, если не отмечено иначе. Формулы дополнения приняты и не являются открытым вопросом P. Q-06 ограничен реальными источниками; Q-10 частично закрыт для прототипа. Рекомендация не является согласованием. Владельцу проекта нужно назначить конкретных ответственных за Сферу, Time, 1С, доступ и эксплуатацию. Архитектура допускает варианты через порты, но не обещает, что смена мастера данных бесплатна.

ID / требование Что нужно решить и варианты Рекомендация Что блокирует / точка расширения Проверяемый результат ответа
Q-01 / INT 01 Проект = рабочая группа, сделка, смарт-процесс или другой реестр? Какое поле РП, несколько ли РП, какие статусы закрывают проект? Рабочая группа, если это существующий реестр проектов Time; OWNER_ID не считать РП без подтверждения Живой импорт проектов и project grants; ProjectSource + ExternalIdentity 2–3 обезличенных проекта с ID, РП, полями, стадиями; правило task→project, включая перенос задачи
Q-02 / INT 03 Мастер заявок/назначений: сервис либо Bitrix? Нужна ли запись копии обратно? Сервис — единственный writer решений; Сфера read-only, без dual-write Утверждение модели хранения и пакетов решений; при ином мастере пересмотр ADR-0001, persistence port Явный выбор мастера, потребители копии и нужные поля/события, если копия требуется
Q-03 / NFR 01, INT 04 Кто видит C, расход, разницы, превышение, историю ставок? РП только R или всё? Может ли РМ подтверждать убыток без C? До разрешения C и все выводимые из неё значения скрыты от РП; РМ с отдельным viewCost; без C согласование блокируется Рабочие финансовые ответы и полномочия; Access policy/DTO projection Матрица полей × ролей, включая CSV/ошибки/before-after; максимальный возраст ставки
Q-04 / §30 Где расходные ставки: 1С, иной реестр или ручной справочник? ₽/ч или месячная сумма, валюта, периоды, правило конверсии? Серверный снимок часовой C на дату согласования как в прототипе; не выводить C из оклада без правила Живой импорт C; OneCSource/CostRateSource API/выгрузка с обезличенной схемой, владельцем, validFrom/To, mapping сотрудника, правилами новых ставок и округления
Q-05 / §30, FR 61–64 Контракт Time: endpoint, права, ID записи, пользователь/задача/дата/часы, исправления, удаления, coverage, закрытие периода. Импорт >2 FTE допустим? Ручной ввод и correction? Сохранять исходные часы; полный snapshot окна и явное покрытие; Time приоритетнее ручного fallback в покрытой области Факт рабочего плана/отчётов; TimeSource, ActualCoverage Примеры обычной, исправленной, удалённой записи и полного пустого периода; mapping task→project и приоритет источников
Q-06 / NEW-03, §30 Принято: опубликованный календарь 2026–2027, отпуск/больничный внутри H, P=FTE×H; вопрос «учитывать ли» закрыт. Для рабочего сервиса нужны источник/регион/график, mapping сотрудника, даты и coverage обоих отсутствий, версии и реальная схема выгрузки. Получить подтверждённый контракт Calendar/AbsenceSource; нули только при подтверждённом покрытии; сводные часы демо не выдавать за точные кадровые даты Только живое подключение календаря/отсутствий; не блокирует прототип. Новая политика исторического пересчёта/денег вне дополнения Обезличенный год и примеры отпуск/больничный/пересечение/полный ноль/пропуск, источники версий и mapping
Q-07 / INT 02 Какие подразделения/люди входят в пул, какая основная принадлежность при нескольких отделах? Роли по полю, mapping должностей или вручную? Нужны ли дизайнер/DevOps/РП как ресурс? Утверждённый mapping шести ролей ТЗ; неизвестные на ручную разборку, права входа отдельно Живой подбор и статистика; RoleMapping/DepartmentSelection Поля источника, таблица mapping и ответственный; решение по локальным кадровым сведениям против импортированных
Q-08 / NFR 01, INT 04 Bitrix OAuth или корпоративный SSO? Кто назначает РМ, передаёт проекты/заявки, что при нескольких РП? TTL сессии, допустимый возраст данных доступа при outage? Bitrix OAuth для совпадения со задача №215, если нет обязательного SSO; fail-closed после утверждённого freshness доступа; передача портфеля аудируется Production-вход и выдача прав; IdentityProvider/AccessGrant Issuer/redirect/scopes без секретов, порядок регистрации приложения, процедура grant/revoke/передачи и сроки
Q-09 / NFR 03 Сколько пользователей/сотрудников/проектов/лет, пиковая конкурентность? RPO/RTO, retention персональных и сырых данных, сроки удаления, окно работ, SLO и владелец алертов? Сначала измеримый профиль нагрузки и restore drill; Postgres backups и наблюдаемость до запуска Production readiness, а не подготовку документов; конфигурация backup/retention/alerts Утверждённые числа, владельцы, сценарий восстановления и критерий нагрузочного теста; чисел из ТЗ нет
Q-10 / анализ ТЗ Подтвердить отсутствующие §8–12/AC 01–40 и ошибочные ссылки; ЦФО/подразделения прототипа уже показывают введённые значения по принятому решению; Требуется перенос данных старого прототипа? Ссылки по фактическим заголовкам; показывать известные поля как prototype-v2; демо не импортировать в prod Только перенос реальных данных и уточнение исторических разделов; импорт как отдельный пакет при необходимости Уточнённая версия ТЗ либо принятые отклонения; перечень реальных переносимых данных
Q-11 / §1, §6 Масштаб FTE и денежных полей, округление сумм, политика валют? decimal, RUB, FTE до 8 знаков; отображение денег до копеек, половина вверх; не округлять часы до суммы Финальный числовой контракт; типы Decimal и presenter Однозначные тестовые примеры на дробные часы/копейки и утверждённый способ округления

Что считается разрешением на следующий этап

Утвердить spec.md с явно принятыми отклонениями от прототипа (ID/права/журнал/версии и рабочие подключения; формулы дополнения уже приняты). Q-02 определяет допустимость предложенной архитектуры в целом. Остальные неизвестные можно отложить только вместе с исключением соответствующего живого подключения из пакета; фейк не считается готовой интеграцией. После ответов обновить spec, ADR, flows и tests одной документальной ревизией, затем приступать к утверждённому пакету. Список реализации в tasks.md не создаёт задач и не назначает исполнителей автоматически.

service/docs/spec.md

Спецификация рабочего сервиса ресурсного планирования

Редакция 13.10.2026 по дополнению NEW-01–09 и реализации прототипа 5213150. Расчётное поведение ниже принято для прототипа; инфраструктура, авторизация, интеграции и хранение рабочего сервиса остаются предложениями. QA прототипа завершена 13.10.2026; проверка рабочего сервиса выполняется после его реализации. Расчётный контракт, матрица покрытия.

Статус: предложение к утверждению, редакция 13.10.2026 по дополнению. Это спецификация поведения, а не разрешение начать реализацию. Принятые формулы и NEW-01–09 не требуют повторного согласования; рабочий сервис начинается после утверждения его архитектуры и относящихся вопросов подключения из tasks.md. Код в этой задаче не создаётся.

Как читать действующие требования

Это единая спецификация: изменения от 13.10.2026 включены в REQ-008/009/011/018/024/026/027 и уточнения ниже. Читать отдельное дополнение не требуется. При противоречии с историческим ТЗ v1.7 применяется эта редакция. Расчётные детали находятся в calculation-contract.md, проверки — в testing.md. Идентификаторы NEW сохранены только для прослеживаемости.

Цель и источники

РП запрашивает ресурсы; ресурсный менеджер (РМ) назначает сотрудников и принимает решения; согласованные назначения образуют единый план. Рабочий контур добавляет вход, серверные права, справочники, факт и журнал без изменения смысла формул прототипа.

База: ТЗ v1.7 от 09.10.2026, фактические заголовки §1–7, §13–30; ../../tz-v1.7/tz.md; анализ ../../analysis/{tz-issues,gap,plan}.md; прототип ../../prototype-v2/js/{calc,api,seed}.js, commit 5213150. Отсутствующие §8–12 и AC 01–40 не считаются восстановленными: критерии ниже выведены из FR. Ошибочные ссылки ТЗ на §31–32 читаются по таблице в tz-issues.md. Документы не требуют доступа к исходным путям для чтения требований.

Участники: РП (pm) — свои заявки и назначения разрешённых проектов; РМ (rm) — весь пул и решения. Технический оператор обслуживает подключения, но не получает автоматически роль РМ или расходные ставки. Должностная роль специалиста и право доступа пользователя — разные понятия.

Требования и проверяемые критерии

Каждая строка — отдельное требование; имя будущего теста содержит [REQ-NNN]. Уточнения рабочего контура помечены «предложение», окончательное решение — open-questions.md.

ID Источник Требование и критерий приёмки
REQ-001 §3–4, FR 01–02 Заявка на один проект: 1–50 строк, 1–24 уникальных месяца на строку; форма предлагает 1–12. При пустом проекте, строках или повторе месяца сохранения нет. Создатель и РП определяются из разрешённого контекста, подмена имени не меняет владельца.
REQ-002 FR 01–04 Роль из согласованного справочника; сотрудник необязателен, но соответствует роли; R > 0 и ≤ 1 000 000 ₽/ч; месячный FTE 0…1 и хотя бы один > 0. При смене роли сотрудник сброшен; совпавшие месяцы сохраняются, новые равны 0; комментарий ≤ 1000 символов. Новая заявка pending не резервирует часы.
REQ-003 §5, FR 05–06, 10 Только pending допускает решение. РМ может изменить сотрудников, FTE и R, но не набор ролей; без сотрудника в любой строке согласование невозможно. approved/returned/rejected повторно не рассматриваются.
REQ-004 FR 07 Согласование проверяет сумму FTE каждого рабочего дня по всем проектам, включая невидимые РП. Значение > 1 + 0,00001 блокируется; ровно 1 допускается. Две одновременные заявки на одного человека не могут обе превысить предел.
REQ-005 FR 08, 12–13 При C > R нужен явный allowLoss; изменение сотрудника, FTE или ставки снимает подтверждение. Показываются текст, Δ и сумма превышения. C = R не требует подтверждения. Неизвестная C не подменяется нулём и блокирует решение до получения ставки.
REQ-006 FR 09–10 Возврат и отказ требуют непустую причину ≤ 1000 символов. Возвращённая заявка исправляется созданием новой pending; исходная сохранена, в плане обе ничего не добавляют до согласования.
REQ-007 FR 11, 19; NFR 02 Решение, назначение, версия и запись журнала сохраняются вместе. Устаревшая версия получает конфликт без частичной записи. При сбое введённые поля остаются в форме, повтор запроса с тем же ключом не дублирует решение.
REQ-008 Дополнение NEW-03; §1, §30; FR 34,37 H сотрудника = Hкал − объединённые рабочие часы отпуска/больничного. P = FTE × H; для days — Σ(дневной FTE × доступные часы дня), явный 0 важнее месяца. Октябрь: Hкал=176, отпуск=16, больничный=8 → H=152, 0,5 FTE → 76 ч. Производственный календарь 2026–2027 учитывает праздники/переносы/7-часовые дни/рабочую субботу. Для «На подбор» до выбора сотрудника используется Hкал. Полнота и сводные часы — calculation-contract.md; отдельной нормы продажи нет.
REQ-009 Дополнение NEW-01/02/07; FR 35,40–41 I=P×R, расход=P×C, разница=I−расход; все используют персональные часы REQ-008. Расход после дохода в списке и месячных строках; форма/карточка также показывают разницу периода. Отрицательная разница красная, нулевая/положительная зелёная в итогах формы/карточки; табличный расход красный только при отрицательной разнице. 88 ч, R=4000,C=4300 → 352000/378400/−26400 ₽. Неизвестная ставка или зависимые часы → прочерк в UI, пустая ячейка в CSV; при известном нулевом объёме ставки «На подбор» не становятся нулевыми.
REQ-010 §1, §6, §18 Rср=Σ(P×R)/ΣP; Cср аналогично. При 40 ч ×4000 и 48 ч ×5000 Rср=4545,45 на экране. Старые месячные R сводятся с сохранением дохода; округление только при отображении/денежном результате, не до суммирования.
REQ-011 Дополнение NEW-05/06; FR 14–17,33 План: сотрудник → проекты, 6 месяцев, единицы FTE/ч/%, поиск и раскрытие. Σ меняет единицу: часы — ΣP, FTE·мес — сумма месячных FTE, % — ΣP/ΣH×100. Нижний «Итог» учитывает каждого видимого сотрудника один раз, проекты — только расшифровка; свёртка не меняет итог. Поиск проекта оставляет полную загрузку найденных сотрудников, Итог равен этим строкам. Фильтры свободного ресурса/имени и область РП соблюдены; неизвестный FTE не считается свободным. Только approved первичных заявок и применённые изменения. Первый месяц в демо не позже 2027-07.
REQ-012 FR 16, 32, 38; NFR 01 РП получает лишь свои заявки в разрешённых проектах и соответствующие назначения; общий сотрудник или проект не раскрывает заявки другого РП. Фильтры, суммы, CSV, история и детализация подчиняются той же области. РМ видит весь пул.
REQ-013 FR 18, 35, 57 Отсутствующий факт — прочерк. После подключения подтверждённого источника рабочий план показывает его данные с датой актуальности; ручной факт прототипа не притворяется автоматически полученным фактом. Семантика покрытия Time требует Q-05.
REQ-014 FR 20–23 Изменение относится к одному действующему назначению: release/reduce/replace; новых месяцев/ролей нет. Дата ≥ сегодня по Москве, причина непустая ≤ 1000. Для reduce/replace 0 < FTE ≤ 1; замена — другой активный сотрудник той же роли. После даты есть ненулевая загрузка.
REQ-015 FR 24 До даты изменения дневной план неизменен. release обнуляет остаток; reduce берёт min(старый FTE, новый); replace освобождает исходного и задаёт новый FTE замене до конца исходного периода. Нулевые месяцы остаются. В эталонном ноябре 2026 вывод 0,5 FTE с 16.11 даёт 35,5 ч и 35,5/159 FTE при подтверждённых нулевых отсутствиях.
REQ-016 FR 21, 25–28 На назначение не больше одного pending-изменения; создание не меняет план. У решения целевые параметры неизменяемы. Отказ/возврат освобождает возможность новой заявки, исходная история сохраняется. Завершённое назначение доступно только для просмотра.
REQ-017 FR 29–31 При решении сверяется версия исходного назначения и снимок; изменение исходника блокирует устаревшее решение. Замена наследует R, C берётся с сервера для нового сотрудника; итоговый план повторно проверяется по дням, включая allowLoss.
REQ-018 Дополнение NEW-04/08/09; FR 36–38 Отчёты: Утилизация, Финансы, Распределение, Некоммерческие проекты, Статистика (последние два перенесены внутрь Отчётов). Утилизация: общий фильтр месяц/роль/проект/имя для трёх показателей: план P/N, коммерческий план Pком/N, факт A/N. Коммерческие проекты — категория «Коммерческий» ИЛИ тип commercial; ограничивается только числитель. При неполном факте, неизвестной/нулевой N — прочерк с пояснением. Октябрь N=1040, P=432, Pком=256; ноябрь N=954,P=A=477 → 50%. Существующая пустая месячная сводка planSummary даёт 0%, новые кольца при N=0 дают прочерк. 12 недель: неделя с датами до 2026 целиком неизвестна, 2025 не достраивается.
REQ-019 FR 39–41, AC 44 Финансы детализированы до назначений; средние взвешены часами; превышения считаются по строкам с C>R, а не по средней. 80ч×4000 и 40ч×6000 при C=4300 дают Rср=4666,67 и одну строку превышения.
REQ-020 FR 42–45 Распределение объединяет назначения сотрудника на проекте; состояние имеет допуск 0,001 п.п.; дата освобождения — следующий календарный день за последним ненулевым рабочим днём всего плана проекта. Последний день 27.11.2026 → 28.11.2026. ЦФО и подразделение показывают введённые значения, при отсутствии — прочерк. Это принятый отдельный фикс.
REQ-021 FR 38, 45; AC 48 CSV поддержанных отчётов соответствует отфильтрованной таблице, UTF-8 BOM, разделитель ;, десятичная запятая, даты YYYY-MM-DD; значения с разделителем экранируются. Выгрузка не расширяет права.
REQ-022 FR 46–49 Некоммерческий отчёт: план/факт, 3/6/12 месяцев, подразделение и имя, синхронное раскрытие. Итоги FTE суммируются. Нет типов проектов → сообщение о настройке; есть проекты, нет плана → 0. Любой пропуск факта делает итог неполным, известная детализация остаётся.
REQ-023 FR 50–53 Статистика считает уникальных людей, не FTE; человек на двух проектах входит в обе группы, но один раз в общий итог. Текущие подразделения/должности/навыки/ЦФО/состояние/дата выхода не объявляются историческим кадровым срезом. Пустые значения → «Не указано».
REQ-024 Дополнение NEW-03; FR 54–56,63–64 РМ меняет локальные сведения и ручной факт/отпуск/больничный. Пустое поле удаляет запись, 0 сохраняется. Факт — одно из fte 0…2 либо hours 0…2Hкал (при известной H также ≤2H); часы сохраняются напрямую, FTE умножается на H. Отсутствие — hours 0…Hкал либо dates (полные календарные дни месяца); поля взаимоисключающие. Объединение рабочих дат вычитается один раз, календарные выходные/праздники дают 0. Положительную сводку и датированные отсутствия одного месяца нельзя смешивать. Сумма отсутствий ≤Hкал; дубли записей и неверные даты отклоняются атомарно. До 10000 записей на поле. Импортированный факт и приоритеты — вопрос рабочего подключения, не реализованное поведение мок-API.
REQ-025 FR 58–60 Категории: «Для внутреннего клиента», «Коммерческий», «НИОКР», «Служебные»; подробная категория выше общего типа. Без неё noncommercial → «Некоммерческий (без уточнения)», без типа → «Тип не указан». Одинаковая норма N используется для каждой категории и не суммируется между категориями.
REQ-026 Дополнение NEW-03; NEW-05/06; FR 60 N=Σ персональных H выбранной группы, без повторного вычета отсутствий; колонки с Nбез удалены. Hкал отображается отдельно. Накопительные часы/нормы сначала суммируются, потом делятся. 80+132 при 160+176 → 63,1%. Для периода 0,5/1/0,75 FTE: ΣFTE·мес=2,25, 388/512×100=75,78125%, не сумма или среднее процентов. Категории не меняют состав знаменателя, нормы категорий не складывают.
REQ-027 Дополнение NEW-03/09; FR 61–64 Факт полон при записи для каждого выбранного сотрудника×проекта×месяца, явный 0 — запись. Нет проектов выбранного типа → часы=0. Для H нужны обе записи отпуск/больничный: пропуск любой делает H, N и зависимые проценты неизвестными; известная календарная норма и явные часы факта остаются. Известный нулевой FTE даёт 0 часов работ даже при неизвестной H. H=0 → 0 плановых часов, процент/FTE от H — прочерк. Отсутствия не входят в A и не вычитаются повторно. Полнота и объединение дат — calculation-contract.md.
REQ-028 INT 01 У проекта стабильный ID и явная связь с РП. Переименование не создаёт дубль. Смена сущности Сферы меняет сопоставление источника, а не идентификаторы заявок; выбор владельца и сущности блокирует живой импорт (Q-01).
REQ-029 INT 02 Сотрудники/подразделения/роли обновляются без удаления согласованных назначений. Деактивация запрещает новый вход и новые назначения; закрытый проект запрещает новые заявки. Частичный ответ источника не считается массовым удалением.
REQ-030 INT 03 Один мастер заявок/назначений; предложение — сервис. Повтор синка справочников не меняет решения. Запись в Сферу отсутствует до отдельного решения Q-02.
REQ-031 INT 04 Секреты доступны только серверному подключению; их нет в странице, журнале ошибок и выгрузке. Повтор синка идемпотентен; недоступность источника сохраняет последний целый снимок и показывает устаревание.
REQ-032 NFR 01 Неавторизованный пользователь не получает бизнес-данных. Роль РМ не выводится из должности. Недоступный объект не раскрывается; операции решений доступны только РМ. Расходные ставки и производные суммы скрыты до явного права (Q-03).
REQ-033 NFR 02 Журнал каждого создания/решения/изменения содержит неизменяемые actorId, время, причину, before/after, версии и признак принятого превышения; первичный запрос не теряется. Нельзя сохранить назначение без записи журнала. Автор не принимается из тела запроса.
REQ-034 NFR 03 Перед рабочим запуском утверждены объём данных, срок хранения, удаление, RPO/RTO и нагрузка (Q-09); восстановление проверено на отдельной БД. Проверка мониторинга обнаруживает искусственный сбой синка и показывает последнее успешное обновление. Числовой SLA пока не установлен.
REQ-035 §30, требование задача №232 Повтор импорта одного набора часов не удваивает факт; корректировка/удаление исходной записи отражается после успешной сверки. Неопознанный сотрудник/проект не сопоставляется по имени автоматически и не попадает в итог как 0.
REQ-036 §1, §30, рабочий контур Календарь и ставки имеют источник и версию. Изменение текущей ставки не переписывает снимок согласованного назначения. Невалидный/неполный календарь не активируется. H использует версии календаря и отсутствий; политика исторического пересчёта рабочего сервиса вне пакета прототипа, её нельзя считать утверждённой дополнением.
REQ-037 требование задача №232 Расчёты, переходы и синк воспроизводимы с фиксированными временем, ID и входными данными без живых внешних систем; каждый модуль проверяется через публичный интерфейс.
REQ-038 требование задача №232 Тестовый вход позволяет проверить обе роли без настоящего входа только в тестовом окружении; в рабочей сборке тестовый маршрут отсутствует даже при переданных флагах включения фейков.

Жизненные циклы

stateDiagram-v2
  [*] --> pending: create или change
  pending --> approved: РМ согласует
  pending --> returned: РМ возвращает с причиной
  pending --> rejected: РМ отклоняет с причиной
  returned --> [*]: новая исправленная заявка с другим ID
  rejected --> [*]
  approved --> [*]: решение неизменно

Назначение появляется только при approved первичной заявки. active/ended — вычисляемые состояния по последнему ненулевому дню и Clock; changePending — отдельный признак. Решение по change атомарно заменяет текущую версию назначения и сохраняет прошлую. Истечение периода не удаляет запись.

Переходы назначения для L1

Состояние Событие и guard Результат
Не существует approved первичной заявки, все проверки пройдены Первая версия назначения; active/ended определяется датой и планом
active, без pending-change proposeChange, есть будущая ненулевая загрузка Та же версия плана + changePending; часы не меняются
active, changePending approve change, снимок/версии совпали Новая версия плана, changePending снят; состояние вычисляется заново
active, changePending return/reject с причиной Версия плана прежняя, changePending снят
active Clock прошёл последний ненулевой день ended, данные и история не меняются
ended proposeChange без будущей ненулевой загрузки Отказ INVALID_TRANSITION, запись не меняется

При выводе с будущей даты статус остаётся active до последнего ненулевого дня; решение не означает немедленного завершения. Если согласование запоздало относительно effectiveDate, оно применяется к сохранённому запросу по правилам снимка FR 29; политика запрета ретроактивного решения, если потребуется, должна быть отдельно согласована — ТЗ ограничивает дату при создании запроса.

Граница утверждения

Нерешённые вопросы рабочего подключения блокируют только перечисленные в open-questions.md рабочие подключения/запуск, а не подготовку документов. До ответов разрешены проектирование и будущие тесты на синтетических данных после утверждения спеки. Исторические дефекты публичного прототипа (имена вместо ID, отсутствие прав, локальное хранилище) не становятся требованиями к сервису.

Уточнения календаря, норм и расчётов

Правила, выведенные из документов (вопросами к автору не являются)

Правило Источник
Hкал(месяц) — часы производственного календаря РФ для 40-часовой пятидневки. Праздники и переносы исключены, предпраздничный день на 1 ч короче, рабочие субботы по переносу считаются. Праздники повторно не вычитаются DOCX п.3; ТЗ tz.md:14 («праздники и отпуска должны быть учтены»); tz.md:726 («норма часов с праздниками и сокращёнными днями»)
H сотрудника = Hкал − часы отпуска − часы больничного за месяц. Отсутствия вводятся только в рабочих часах (≤ Hкал), день, в который совпали оба вида, учитывается один раз. Больничные не прогнозируются: только введённые значения DOCX п.3; границы задачи
Часы дня в дневном плане: P = Σ(доступные часы дня сотрудника × дневной FTE). «× 8» из FR 37/FR 24 — это частный случай дня по 8 ч FR 34, FR 37 с календарём tz.md:726
N = ΣH выбранных сотрудников. N − V не считается: отпуск уже внутри H. Колонки «без отпусков» объединяются с «по норме», доля каждого вида отсутствия считается от ΣHкал; общую долю считают по объединению дат, не по V + B DOCX п.3; формулы FR 60/§26 при новой H
Пустое значение отсутствия не равно 0. Если у выбранного сотрудника за месяц нет записи об отпуске или больничном, его H, N группы и проценты по N показываются прочерком. ΣHкал («норма по календарю») и явно введённые часы факта остаются видимыми; зависимые плановые часы неизвестны. После ввода 0 значение становится известным. В демо явно заполнены подтверждённые значения, включая нули FR 62, AC 61 (видимая величина — календарная норма, как раньше видимой была N)
Накопительно и за период: сначала сумма часов, сумма норм, затем процент tz.md:41, FR 60, AC 60
Σ в плане: FTE — сумма месячных FTE, % — ΣP / ΣH × 100, часы — ΣP Глоссарий Σ tz.md:32; tz.md:41
«Итог» плана — сумма строк сотрудников (строки проектов — их расшифровка, повторно не прибавляются). По FTE — сумма, по % — ΣP / ΣH. При поиске по проекту строки сотрудников, как и сейчас, показывают полную загрузку, Итог — их сумма DOCX п.6 «суммируются значения по строкам»; текущее поведение plan.js
NEW-08 — плановые часы проектов группы «Коммерческий» (категория или общий тип commercial) / N DOCX п.8 «по форме плановой утилизации»; AC 64; FR 36
NEW-09 — A / N × 100; при неполном факте прочерк, при N = 0 прочерк DOCX п.9; FR 61, AC 62
Финансы: расход = P × C, разница = I − расход. Неизвестная C — прочерк, не 0. Цвета: в форме и карточке отрицательная разница красная, остальное зелёное; в таблицах при отрицательной разнице расход красный, иначе без цвета DOCX п.1, 2, 7; FR 35

Календарь в данных прототипа (часы на 40-часовую неделю)

янв фев мар апр май июн июл авг сен окт ноя дек год
2026 120 152 168 175 151 167 184 168 176 176 159 176 1972
2027 120 151 176 175 152 167 176 176 176 168 159 176 1972

Ловушки реализации:

Принятая формула P и денег

Действующая постановка: P = FTE × H сотрудника после вычета отсутствий; доход = P × R, расход = P × C. Повторного согласования не требуется. При дневных изменениях используется сумма дневного FTE × доступные часы дня, исходные месячные ставки сохраняются. Отдельной нормы продажи нет; новый процесс пересогласования не вводится.

Анна, октябрь 2026: календарь 176 ч, отпуск 16 ч, больничный 0 → H = 160. При 0,5 FTE и R/C = 3500/2800: P = 80 ч, доход 280 000, расход 224 000 ₽, утилизация 50 %. Больничный 8 ч → H = 152, P = 76 ч, доход 266 000, расход 212 800 ₽. Изменение введённых отсутствий меняет расчёты прототипа во всех представлениях.

У назначенного сотрудника пустой отпуск или больничный означает неизвестную H: зависимые P, деньги и проценты — прочерк. Явно введённые фактические часы остаются видимыми. Для строки «На подбор» персональной нормы ещё нет: до назначения используется календарь; после выбора сотрудника расчёт использует его H.

Отпуск/больничный: запись {employee, month, hours} — подтверждённые рабочие часы без пересечений; {employee, month, dates} — полные дни с точным объединением дат. Даты праздников/выходных дают 0 ч, сокращённые — 7 ч. Для сводных часов без дат дневная доступность распределяется пропорционально календарным часам: это допущение демо, точные дни задаются датами. Положительные сводные часы и датированные отсутствия в одном месяце не смешиваются: API просит указать оба отсутствия датами. Пустое поле удаляет запись, 0 или пустой массив дат подтверждают отсутствие. API атомарно отклоняет сумму свыше календаря и неверные даты.

Мок принимает фактический fte либо явные hours (одно из двух). FTE преобразуется через персональную H; явные часы не теряются при неизвестных отсутствиях. При согласовании требуется подтверждённая H назначенных сотрудников; сохранение заявки с неизвестной H допускается. Проверка перегрузки использует рабочие дни, в том числе рабочую субботу, и исключает подтверждённые дни без доступных часов.

Источники календаря, проверенные 13.10.2026: 2026, 2027, постановление №1187. Федеральная пятидневка на 40 ч; региональные календари в демо не включены.

service/docs/tasks.md

Пакеты реализации после утверждения

Редакция 13.10.2026 по дополнению NEW-01–09 и реализации прототипа 5213150. Расчётное поведение ниже принято для прототипа; инфраструктура, авторизация, интеграции и хранение рабочего сервиса остаются предложениями. Общая QA всех NEW ещё предстоит. Расчётный контракт, матрица покрытия.

Только план; задач в Multica не создано. Каждый пакет — одна наблюдаемая вертикаль, один MR, тест до реализации (RED → GREEN → refactor). Техлид распределяет работу скваду после утверждения spec/architecture и относящихся Q. Команды ниже будущие, сейчас не существуют и в задача №232 не запускались. Общая команда приёмки каждого MR: pnpm lint && pnpm typecheck && pnpm exec tsx scripts/req-coverage; в первых MR coverage сверяется по явно утверждённому списку REQ пакета, финальная приёмка П11 — по всем REQ без исключений.

Не превращать пакет в горизонтальный слой «вся БД» или «весь UI». При росте объёма уточнить вертикаль до начала разработки, не делать бесконечный MR.

Пакет / ведущая компетенция Вертикаль и результат одного MR Зависимости / ворота REQ Тесты сначала и команда приёмки
П1 / fullstack + QA Войти тестовой личностью и увидеть разрешённый проект: Nuxt/Nitro composition root, схема Principal/Grant/Project/Session, shared contracts, PGlite/Postgres switch, Clock/ID, builders, тестовые scripts/req-coverage, production-negative вход. Минимальная страница и health. Утверждены spec, ADR-0004/0005; живой OAuth не требуется 012,032,037,038 L1 scope policy; L2 grants/session на миграциях; L4 auth и чужой проект; L5 smoke тестового входа; миграции empty; prod-safety. pnpm test:module -- --project access && pnpm test:api -- --project access && pnpm test:prod-safety && pnpm test:migrations плюс L1/access и e2e/access
П2 / backend + UI Сотрудники и проекты из fake/Bitrix в форме выбора: SphereClient совместимый задача №215, ProjectSource, staging/status, ID mapping, деактивация, ручной run, экран справочника/ошибок. П1; Q-01/Q-07 для real adapter; до ответа только контракт фейка, MR не объявлять живой интеграцией 028,029,031,037 L2 два sync не дублируют, обрыв не удаляет; L3 user/department/group pagination/errors; L4 операторский запуск; L5 деактивация; migration справочников. pnpm test:contract -- --project sphere && pnpm test:module -- --project sync && pnpm test:api -- --project sync && pnpm test:e2e -- --grep REQ-029
П3 / backend + fullstack РП создаёт заявку и просматривает свою карточку: Request/Line, точные значения FTE/R, scoped список, исправленная заявка как новая. П1–П2; Q-02 принят, Q-11 числовой контракт 001,002,006,012 L1 границы и pending; L2 create/idempotency; L4 все роли create/get; компоненты FTE по месяцам; L5 создание двух строк; migration Request. pnpm test:domain -- --project requests && pnpm test:module -- --project requests && pnpm test:api -- --project requests && pnpm test:components && pnpm test:e2e -- --grep REQ-001
П4 / backend + fullstack РМ принимает первичное решение, назначение появляется в плане: Calculator из calc.js, cost snapshot (fake source), journal, транзакция и блокировка ёмкости, return/reject, six-month UI с динамической Σ и нижним Итогом без двойного счёта, синтетическим календарём и подтверждёнными отсутствиями. П3; Q-03/Q-04/Q-11 для политики ставок; real 1С не требуется 003,004,005,007,008,009,010,011,030,033 L1 формулы/автомат/property; L2 rollback журнала, версии и неизменность решений после sync; L4 матрица решения/redaction; L5 заявка→план; Postgres два approve; компоненты плана; миграции. pnpm test:domain && pnpm test:module -- --project decisions && pnpm test:api -- --project decisions && pnpm test:postgres -- --project approval && pnpm test:e2e -- --grep REQ-003
П5 / backend + fullstack Изменить назначение: preview/release/reduce/replace, before/after, история, защита снимка, пересчёт плана и даты освобождения. П4 014,015,016,017,033 L1 дневные инварианты; L2 один pending и stale snapshot; L4 чужое назначение; L5 замена с превышением; Postgres pending-race; миграции версий. pnpm test:domain -- --project changes && pnpm test:module -- --project changes && pnpm test:api -- --project changes && pnpm test:postgres -- --project changes && pnpm test:e2e -- --grep REQ-017
П6 / fullstack + QA Плановые отчёты и CSV: три показателя утилизации, финансы, распределение, свободный ресурс, недельная граница календаря, scope/redaction. П4–П5; поля распределения уже приняты для прототипа; Q-10 только наследие 018,019,020,021 L1 AC 41–48; L2 выборки/суммы; L4 CSV и утечки C; L5 отчёт; компоненты только сложной таблицы. pnpm test:domain -- --project reports && pnpm test:module -- --project reports && pnpm test:api -- --project reports && pnpm test:e2e -- --grep REQ-021
П7 / fullstack + backend Управленческие отчёты с ручным fallback: некоммерческие, статистика, типы, отпуск/больничный/пересечения и полнота обоих видов, общие фильтры. П6; Q-05/Q-07/Q-10 приоритеты и поля 022,023,024,025,026,027 L1 AC 49–66, property null/0; L2 partial data, уникальность; L4 PATCH только РМ; L5 полный/неполный отчёт; миграции local data. pnpm test:domain -- --project completeness && pnpm test:module -- --project reporting-data && pnpm test:api -- --project reporting-data && pnpm test:e2e -- --grep REQ-027
П8 / integrations + QA Факт Time до экрана: adapter, raw entries, task mapping, coverage, исправления/удаления, переключение источника отчётов/плана, stale UI. П2,П7; Q-01/Q-05 утверждены, captures предоставлены 013,031,035 L3 Time и task mapping; L2 идемпотентность/reconcile/quarantine; L4 нет доступа РП к чужому факту; L5 fake Time outage→recovery; миграции actuals. pnpm test:contract -- --project time && pnpm test:module -- --project actuals && pnpm test:api -- --project actuals && pnpm test:e2e -- --grep REQ-035
П9 / integrations + backend Календарь/отсутствия/ставки от согласованного источника до превью и решения: версии/coverage, короткие дни, рабочая суббота, union отсутствий, unknown, сверка rateVersion. П4–П5; Q-03/Q-04/Q-06/Q-11, схема 1С 008,031,036 L1 короткий день/выходной; L3 1С; L2 полная активация версии и неизменность cost snapshot; L4 RATE_CHANGED; Postgres rate/approve race; миграции calendar/rate; L5 превью после календаря. pnpm test:contract -- --project onec && pnpm test:module -- --project calendar-rates && pnpm test:postgres -- --project rate-race && pnpm test:api -- --project rates плюс L1/calendar и e2e/calendar
П10 / backend + security review Настоящий вход выбранного IdP → scoped план: OAuth/SSO adapter, безопасные cookies/CSRF, grant/revoke, freshness и передача портфеля по принятому правилу. П1–П2; Q-08 и регистрации приложения; П6–П9 для полной матрицы 012,029,032,038 L3 identity captures; L2 sessions/revoke; L4 полная роль×эндпоинт×поля; prod-safety fake bypass; L5 IdP stub. pnpm test:contract -- --project identity && pnpm test:api && pnpm test:prod-safety && pnpm test:e2e -- --grep auth
П11 / platform + QA Эксплуатация синка и восстановление: worker lease/fencing, расписание тех же run, статус/алерты, backup/restore runbook, нагрузочный профиль и финальная трассировка. Все пакеты; Q-09 утверждён 007,031,034,037 L2 crash/replay; L3 outage; L4 ready/status; Postgres fencing; L5 stale; migrations empty+snapshot; восстановление и нагрузка по принятому профилю. pnpm test:postgres && pnpm test:migrations && pnpm test:e2e && pnpm test:restore && pnpm test:load && pnpm exec tsx scripts/req-coverage

П1 добавляет именованные vitest projects/Playwright grep tags для указанных команд; если suite использует иной runner CLI, команда в tasks.md меняется вместе с MR, а не остаётся невыполнимой. Все REQ в таблице пишутся с префиксом REQ- в тестах. L3 не нужен там, где пакет не меняет внешний adapter; migrations требуются каждому пакету со схемой. L4 и финальная матрица расширяются в том же MR, где появился endpoint. Полные L1/L2/L4 и миграции изменённой схемы всегда обязательны в CI, даже если локальная строка показывает targeted run.

Приёмка пакета командой

В описании MR: пользовательский результат, принятые Q/ADR, RED/GREEN команды и итог, изменение контрактов/миграций, screenshot только для изменённого UI. Reviewer прогоняет указанные команды на чистой БД, QA воспроизводит сценарий на fake-стенде, технический владелец проверяет прямой вызов модуля без браузера. Фикстурный L3 доказывает обработку заданного формата, но не работоспособность неизвестного live API. Весь сервис готов к рабочему запуску только после закрытия соответствующих вопросов и приёмки П11; никакой код не начинается только на основании этого списка.

Реализованный прототип и следующий шаг

В prototype-v2 5213150 все NEW-01–09 реализованы последовательными пакетами, матрица показывает файлы и проверки. Этот статус не выполняет П1–П11 сервиса. Фейковый выбор РМ/РП, localStorage, синхронный mock API и текущие входы отсутствий остаются границами демо. Новые процессы пересогласования/замещения/исторической фиксации денег в пакеты дополнения не добавлять. Следующая приёмка — общая QA всех NEW и риск-ревью, которые пока не завершены.

service/docs/testing.md

Проверки и требования к тестируемости

Редакция 13.10.2026 по дополнению NEW-01–09 и реализации прототипа 5213150. Расчётное поведение ниже принято для прототипа; инфраструктура, авторизация, интеграции и хранение рабочего сервиса остаются предложениями. Общая QA всех NEW ещё предстоит. Расчётный контракт, матрица покрытия.

Это план будущих тестов сервиса, не отчёт об их выполнении. В задача №232 проверяются документы и рендер схем; исходного кода сервиса нет. REQ из spec.md включается в имя каждого проверяющего теста, например [REQ-004] два параллельных решения не превышают дневную загрузку.

Уровни

Время — бюджет обратной связи на типичном CI runner, а не измерение и не SLA. Если превышено, искать медленные фикстуры/интеграции, не выкидывать проверки прав и расчётов.

Уровень Что проверяет Что настоящее / что подменяется Инструмент Где и ориентир
L1 домен Формулы §1, finite-state Request/Assignment, C>R, FTE, дни изменения, null/0, округление Чистые функции; календарь, Clock, ID и ставки — значения/фейки; БД/сети нет vitest + fast-check, таблицы AC и property-based Локально/каждый CI, до 15 с
L2 модуль Публичные интерфейсы шести модулей, транзакции, FK/unique, idempotency, rollback, scope, снимки Реальная PGlite in-memory, схема из тех же миграций Drizzle; fake внешние порты vitest, отдельная БД на тест Локально/каждый CI, до 60 с
L3 контракт адаптеров Bitrix/Time/1С/IdentityProvider: parse, next/cursor, 200+error, timeout, 429, исправления, деактивация, missing fields Реальный HTTP-адаптер, локальный stub HTTP, записанные обезличенные ответы, без credentials vitest, фикстуры JSON и HTTP stub Локально/каждый CI, до 30 с; opt-in задача №27 отдельно до 2 мин
L4 API Коды/DTO/zod, CSRF, версия, роль×эндпоинт, field redaction, pagination Настоящий Nitro в тесте, PGlite, fake источники и test identity vitest + @nuxt/test-utils, HTTP запросы Локально/каждый CI, до 90 с
L5 e2e 5 сценариев ниже, формы и запросы, состояние после сбоя Собранный test-вариант приложения, сценарные seeds, fake HTTP Sphere/Time/1С Playwright Локально/CI, до 3 мин
Компоненты Только сложная таблица плана и ввод FTE по месяцам: клавиатура, сохранение совпавших месяцев, явный 0, раскрытие Vue-компонент и composable; API fake; расчёты отдельно L1 vitest + @vue/test-utils Локально/CI, до 20 с
Миграции Пустая БД и snapshot предыдущей схемы с заявками/историей/частичным фактом; неизменность сумм/связей Реальные PGlite и Postgres, все SQL migrations vitest + Drizzle migrator, CI Postgres service CI и локально по флагу, до 90 с
Postgres concurrency Два соединения: решения одному сотруднику, два pending-change, lease/fencing, конфликт sync/rate/approval, rollback журнала Настоящий Postgres, те же модули; внешние порты fake vitest и CI Postgres service Каждый CI, локально по DATABASE_URL, до 60 с
Production safety Fake-login отсутствует в prod build даже с SPHERE_FAKE/TIME_FAKE, секреты отсутствуют в JS/ошибках, readiness требует Postgres Production build + Postgres; no live external credentials HTTP smoke через vitest/Node, проверка bundle Каждый CI, до 45 с без времени сборки

PGlite — настоящая embedded БД для модульных тестов, но не доказательство конкурентности Postgres. Нельзя проверять race простым Promise.all в одном PGlite connection. Для Postgres тест управляет двумя соединениями и барьерами: обе транзакции стартовали, одна ждёт lock; результат ровно одно успешное решение, второе получает OVERLOAD/конфликт, а итог ≤1 FTE.

Сценарии и инварианты

L1: воспроизвести AC 41–66 из ТЗ и соответствующие createExamples из seed.js, ноябрьский пример FR 24 на производственном календаре: 0,5 FTE с 16.11.2026 → 35,5 ч при H=159 и нулевых отсутствиях. Проверить рабочую субботу 20.02.2027 на 7 ч и недельную границу января–марта 2026 без нормы 2025. Reference calc.js используется для сравнительных примеров, но тесты не должны копировать реализацию и подтверждать её ошибку.

Property-based, минимум 1000 примеров для ядра в CI, seed и минимальный counterexample печатаются при падении:

L2 проверяет модуль через интерфейс, не приватные функции: сбой вставки DecisionLog откатывает approve; повтор commandId не добавляет журнал; команду с другим hash отвергает; stale request/assignment/rate версии не теряют данные. Синк деактивации не удаляет Assignment/DecisionLog и отзывает Session.

Матрица L4: генерируется целиком

Каждый маршрут из architecture.md обязан попасть в таблицу тест-кейсов; CI сравнивает реестр маршрутов/операций с матрицей, а не полагается на её ручную полноту. Роли теста: anonymous, pm-owner, pm-same-project-other-manager, pm-foreign-project, rm, operator-no-business-grants, revoked. Для каждой — чтение/запись и попытка подмены ID/filter/body. РМ и operator независимы.

Операция anonymous pm-owner pm другого РП/проекта rm operator без бизнес-прав
requests list/get, assignments, plan 401 200 в своём scope Список без чужих; get чужого 404 200 весь пул 403
create request, preview/change assignment 401 201/200 по правилам 404 чужой объект/проект; подмена managerId не расширяет scope 201/200 403
approve/return/reject 401 403 для своей заявки 404 если чужая, иначе 403 200 или доменная ошибка 403
reports JSON/CSV 401 200 scoped, C и производные скрыты без viewCost Чужие строки/итоги отсутствуют 200, поля согласно viewCost 403
patch report-data 401 403 403 200 только локальные поля 403
sync status / enqueue 401 403/403 403/403 200/403 без operator 200/202
auth begin/callback/logout Redirect либо отказ callback Одноразовый state, logout revokes Те же правила сессии Те же правила сессии Те же правила сессии
health live/ready Только минимальное состояние 200/503 То же То же То же То же

Revoked всегда 401 на бизнес-API. Для rm без viewCost чтение маскируется; approve, требующий недоступной финансовой информации, запрещён до решения Q-03. Для JSON, CSV, before/after, totals и ошибок проверяется отсутствие C, plannedCost, costExcess, rateDifference и других производных скрытой C, а не только поле costRate. Недоступный проект не раскрывается счётчиком страниц.

L5: только ключевые сценарии

  1. РП создаёт две строки → РМ назначает и соглашается → план обновлён, журнал содержит реального автора, другой РП не видит заявку.
  2. РП предлагает замену с даты → план до решения прежний → РМ подтверждает превышение → дневной план/освобождение/история изменены один раз.
  3. РМ открывает отчёт с неполным фактом/отпуском/больничным → прочерки → fake Time полный снимок и отпуск=больничный=0 → правильные итоги/CSV; РП видит только свою область.
  4. Два браузерных контекста редактируют одну заявку → второй получает 409, его ввод сохранён; повтор первого commandId не дублирует решение.
  5. Fake источник становится недоступен, затем деактивирует сотрудника → виден stale; после восстановления новая заявка на него запрещена, прошлое назначение видно, сессия отозвана.

Fixture/builders и изоляция

Builders исходят из seed.js (6 сотрудников, 3 проекта, demo-1…3, createExamples), добавляют стабильные project/manager/line IDs и явно связывают роли. Один сценарный каталог используется в L2/L4/L5 и стенде; тесты не создают разные копии бизнес-правил. Clock фиксируется на дате сценария, IdGenerator детерминирован. После каждого теста БД закрывается и удаляется собственный temporary storage; тесты не делят global state. Сценарии не содержат реальные ФИО/ставки/секреты.

Записанные API fixtures хранят version, provenance (какой метод, дата записи, synthetic или sanitized capture) и ожидаемые DTO. Сейчас Time/1С доступны только как проектируемые synthetic fixtures: их нельзя считать доказательством совместимости живого API. Перед подключением заменить/дополнить подтверждёнными обезличенными captures и получить контрактный smoke. Опциональный задача №27: CONTRACT_LIVE=1 и только явно указанный тестовый host; не запускается по умолчанию, тест отклоняет боевой host.

Fake-login route включён compile-time только в test build и проверяет NODE_ENV=test также во время исполнения. Отдельный production build проверяется с намеренно переданным fake-флагом: маршрут 404, произвольная test-cookie не аутентифицирует. Публичный демонстрационный стенд — либо test build только с синтетическими данными и явной плашкой, либо обычный build с настоящим тестовым IdP; никакого тестового входа в рабочую сборку.

Покрытие и команды будущего проекта

Порог только domain/**: lines/statements/functions ≥90%, branches ≥85%. Основание: ошибки ветвлений решения/полноты опаснее покрытия handlers; 100% по проекту поощряет бессмысленные тесты прокладок. Критические правила REQ-004/005/007/012/017/032/033/038 должны иметь все перечисленные негативные случаи независимо от процентов. Generated DTO/миграции не исключают доменный файл из coverage по удобству; исключения документируются и ревьюятся.

pnpm test:domain, test:module, test:contract, test:api, test:components, test:e2e, test:migrations, test:postgres, test:prod-safety — будущие scripts, создаваемые в П1. pnpm exec tsx scripts/req-coverage сверяет spec.md с реально собранным vitest/Playwright test inventory и JUnit результатом, а не grep произвольного комментария. Неизвестный REQ, отсутствующий, skipped/todo-only или отсутствующий успешный тест — ненулевой exit. Несколько REQ в имени допустимы при реальной проверке каждого. Новое требование не считается покрытым случайным упоминанием в фикстуре.

CI: frozen install → typecheck/lint → L1–L4/компоненты/req-coverage → миграции/Postgres → build/test-production safety → L5. До живых интеграций CI не имеет их credentials. Сначала в пакете пишется падающий тест критерия (RED), затем код (GREEN), затем refactor; MR содержит команды и вывод, а не только проценты.

Действующая матрица дополнения и проверка прототипа

NEW → код → проверка → документ относится к реализации 5213150, а будущие L1–L5 выше — к рабочему сервису. Наличие тестов прототипа не доказывает готовность интеграций, прав или общей QA.

При переносе критериев: Hкал=176,V=16,B=8 → H=152,P(0,5)=76; одинаковые суммы списка/формы/карточки/CSV; unknown против 0; объединение одинаковой даты/праздника/короткого дня; атомарный отказ свыше Hкал; явные часы факта при неизвестной H; ΣFTE·мес=2,25 и 388/512×100; Итог без проектов/после свёртки/по scope; коммерческие и фактические показатели с одним составом людей. Покрытие факта требует всех employee×project×month, отсутствий — employee×month×kind. P и деньги не имеют второго знаменателя продажи.

Регрессионные критерии принятого поведения

Идентификаторы NEW сохранены для связи с историей. Требования включены в spec.md; этот список задаёт повторяемые сценарии проверки.

Числа приведены для календаря выше и принятой персональной нормы.

service/docs/validation.md

Проверка документальной редакции дополнения

Дата: 13.10.2026. Эталон прототипа — 5213150, дерево и код прототипа не изменены этим пакетом. Проверяются документы, их расчётные примеры и публикация; общая QA всех NEW и риск-ревью ещё предстоят. Будущие тесты сервиса из testing/tasks не запускались: серверного кода здесь нет.

Локальные проверки

Производные и опубликованные страницы

Архитектура и схемы — прежний URL. Расчётный контракт, матрица NEW, analysis — читаемые HTML без CDN. Исходные Markdown/ADR также доступны из страницы схем. Статический прототип остаётся на прежнем URL, его публикация не меняется документальной редакцией.

Проверка публикации выполняется Playwright-скриптом workdir verify-published-docs.cjs: размеры/число SVG, уникальность ID и внутренние якоря, HTTP-ответы связанных страниц/Markdown, ошибки JS, viewport 1440/320/390 и скролл схем/таблиц внутри карточек. Команда прошла: 11 SVG, 20 уникальных внутренних HTTP-целей со статусом 200, уникальные ID и все проверенные якоря корректны, ошибок JS/console 0. На 1440/320/390 px переполнения страницы нет. Отдельная HTTP-проверка 18 внешних/опубликованных ссылок дала 200 для каждой; это проверка доступности, не новое подтверждение содержимого API. Снимки приложены в отчёте пакета. Старые результаты проверки архитектуры на предыдущей редакции не выдаются за новые.

Повторение сборки

В service/scripts находятся инструменты только документации, не сервер приложения. Требуются установленный mermaid-cli/marked и Chromium; окружение передаётся аргументами, новых переменных сервиса нет.

node scripts/render-docs.mjs docs /path/to/render-work /path/to/mermaid-cli/src/cli.js /path/to/puppeteer.json /path/to/marked/lib/marked.esm.js ../analysis
node scripts/verify-docs.mjs docs ../prototype-v2 ../analysis
node --check scripts/render-docs.mjs
node --check scripts/verify-docs.mjs
git diff --check

Puppeteer config содержит executablePath и args. Для изолированной среды без системного sandbox используется аргумент --no-sandbox. Выход docs/diagrams.html и docs/reference.html, analysis/reference.html — производные исходных Markdown; исходные Mermaid показаны в раскрывающихся блоках. Для обновления только прозы можно использовать последний аргумент --reuse-svg: он разрешён лишь если сохранённые Mermaid-исходники совпадают и соответствующие SVG существуют.

Граница приёмки

Формулы/NEW прототипа приняты, но серверная модель ID/Decimal/версий, безопасность, подключения, SLA и политика истории сервиса остаются предложениями и нерешёнными рабочими вопросами. Q-06 не спрашивает о формуле P или необходимости праздников/отсутствий. Дополнение не утверждает новую фиксацию денег или пересогласование. Remote сервиса отсутствует; изменения передаются локальной веткой и коммитом. Общая QA всех NEW не закрыта этим документальным пакетом.