Действующие требования собраны в спецификации; отдельное дополнение читать не требуется. Расчётный эталон 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; основание расчёта, не личный график сотрудника.
Дата бизнеса — календарная дата 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-v25213150. Все девять реализованы принятыми пакетами; эта матрица связывает требования с существующими файлами/проверками, но не объявляет общую QA и риск-ревью завершёнными. Исходный v1.7 и старые картинки сохранены как история. Расчётный контракт, спецификация, проверки, будущий сервис.
Все пути к реализации и тестам ниже — относительно prototype-v2, документы — относительно service/docs. Unit — Node npm test; browser-файлы — функции для Playwright на опубликованной странице. Проектируемые L1–L5/HTTP-интеграции сервиса не считаются этими тестами выполненными.
NEW
Принятое поведение / REQ
Код и владелец
Проверки существующего прототипа
Документ
NEW-01
Доход/расход/разница периода; отрицательная красная, ≥0 зелёная, неизвестная C прочерк; REQ-009
Отдельный принятый старый дефект: ЦФО/подразделение и 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 не выходит.
Прячет: конечный автомат, validation ролей/ставок, idempotency, проверку версии, снимки первичной заявки, журнал решений и одну транзакцию изменения назначений. Вызывает: Nitro API. Проверяется: L1 автомат и правила решения; L2 через create/decide с настоящей БД; L4 права и ошибки, L5 заявка→план. Requests владеет Request/ResourceLine/AssignmentChange/DecisionLog/CommandReceipt. Вызов Assignments.applyDecision происходит внутри его Tx, а не отдельным HTTP-запросом.
Прячет: дневные интервалы, состояния active/ended, единственное ожидающее изменение, блокировки общей ёмкости людей, версии назначений и согласованную проекцию плана. Вызывает: API для чтения/превью, Requests для решения; ApprovedDecision — внутренний тип модуля, клиент его не создаёт. Проверяется: L1 дневные преобразования; L2 смена версии без двойного учёта и откат; Postgres-проверка конкурентного решения; L4 область РП; L5 замена. Владеет Assignment/AssignmentVersion/AssignmentDay; изменение существующей версии запрещено, текущий указатель меняется транзакционно.
Прячет: формулы §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-порт отсутствий ещё не реализованы.
Прячет: безопасную область и агрегации, категории проектов, actual coverage, отсутствие факта, текущие кадровые атрибуты, проекцию ставок по viewCost, экранирование CSV. Вызывают API и UI через API. Проверяется: L1 примеры AC 41–66 в Calculations; L2 real DB фильтры/полнота; L4 матрица прав и отсутствие утечек в CSV; L5 отчёт. Владеет только локальными overrides/категориями/ручными actuals/vacations/sickLeaves; импортированные snapshots принадлежат Sync и читаются, не редактируются. Итоги собираются одной согласованной версией снимков, версии возвращаются в метаданных.
Прячет: 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 как доверенный объект.
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 порты.
Таблицы нормализованы: нет одной JSON-записи State/main, которая блокирует весь сервис. JSON допустим для неизменяемого before/after в DecisionLog и валидированного сырого staging с ограниченным сроком хранения; фильтруемые поля и внешние ключи — колонки.
Уникальность (source, entityType, externalId); FK не позволяют связать запись с неизвестным человеком/проектом. Роли и имена имеют локальные ID.
Версия Request защищает заявку, Assignment.version защищает исходное назначение. Частичный unique index запрещает два pending-change одного assignmentId.
Для решения транзакция сначала блокирует стабильные строки Employee всех затронутых людей в порядке ID (FOR UPDATE), затем Request и Assignment; перечитывает их версии и весь согласованный дневной план этих людей. Все решения используют один порядок. Проверка общей загрузки видит назначения всех проектов независимо от видимости актору, ошибка наружу не раскрывает чужие проекты/ставки.
Read Committed с блокировками Employee достаточен при обязательном использовании того же протокола всеми writer; вставка назначения без этого модуля запрещена. Конкурентность проверяется именно на Postgres, не заменяется последовательным тестом PGlite.
Проверка active/closed, разрешений и rateVersion повторяется в транзакции. Sync обновляет затрагиваемые employee-строки тем же порядком блокировок; при изменении ставки после превью возвращается RATE_CHANGED и требуется новое подтверждение, а не скрытое принятие убытка.
Requests создаёт DecisionLog и новые версии Assignments в одной транзакции; сбой журнала откатывает решение. CommandReceipt уникален по principalId+commandId, хранит hash команды и безопасную ссылку результата; повтор с иным payload → COMMAND_KEY_REUSED. Проверка текущих прав выполняется и при повторе.
Для будущего сервиса предлагаются ссылки на CalendarVersion и AbsenceSnapshot/Coverage в метаданных расчёта. Прототип вычисляет результат по текущим введённым отсутствиям; их изменение меняет P и деньги во всех представлениях. Новая политика исторического пересчёта, фиксации денег и пересогласования рабочего сервиса не утверждена и не входит в дополнение.
Журнал append-only для приложения, с маскированием C и производных значений при чтении без viewCost. Нельзя утечь through before/after. Административное удаление/обезличивание по NFR 03 — отдельный регламент после Q-09, не обычный UPDATE истории.
Заявки сохраняют исходного РП; project_manager даёт доступ к проекту, но не автоматически ко всем заявкам других РП. Передача портфеля — Q-08, отдельная аудируемая операция.
API и отличие от прототипа
/api/v1 — предложение нового контракта, а не обещание бинарной совместимости с прототипом. Старые action=create/approve/return/reject/change сохраняют бизнес-смысл; manager из строки заменён проверенной личностью, lineIndex — стабильным ID, global revision — версиями агрегатов. Старый GET State не переносится в рабочий API, чтобы не отдавать данные всех проектов. Если нужен импорт прототипа, он делается отдельной проверяемой операцией (Q-10).
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, ограниченный размер страниц. Подробности ошибки не раскрывают скрытые ставки.
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 содержат уникальные записи сотрудник×месяц. Каждая запись — одно из:
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 контрактной проверки.
Общий протокол входящих потоков
Scheduler или оператор создаёт SyncRun. Уникальный source+scope+scheduledSlot не даёт дубликат задания; worker берёт lease с TTL и heartbeat. run получает фиксированный asOf и snapshot конфигурации.
Адаптер читает страницы в staging, следует next/cursor конкретного API; не считает короткую страницу концом, если есть next. Сортировка по стабильному ID; cursor и исходные version/hash сохраняются. Bitrix может вернуть ошибку в HTTP 200: проверяется тело error.
Сетевой timeout: предложение 20 с, не более 5 попыток на run, exponential backoff с jitter 1–60 с и учётом Retry-After. 429/5xx — повтор; 401/403 — остановка до исправления credentials; невалидная схема — quarantine и failed. Повтор/задержка инъецируются для детерминированных тестов.
Только полный валидный снимок/страница с подтверждённой границей коммитится с checkpoint в одной транзакции. Для full reconcile удаление отсутствующих допускается только после успешного завершения всей области; обрыв страницы не меняет статус ранее известных записей. Незнакомые ID не сопоставляются по ФИО автоматически.
Lease имеет fencing version: потерявший lease worker не может активировать старый snapshot. Повтор обработки — upsert по внешнему ключу и версии/hash. Snapshot активируется атомарно; отчёт не смешивает старые и новые страницы одного периода.
При недоступном источнике сохраняется последний целый снимок, 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
Цикл дерева/неизвестный 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 статусов проекта; история сохранена
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
employee/month, vacations и sickLeaves; одно из hours либо dates полных дней. В будущем источник, kind, sourceVersion и coverage; endpoint 1С неизвестен
Ручное сохранение; импорт только после получения контракта
В демо по запросу; частота рабочего источника не утверждена
employee+month+kind; записи уникальны; даты объединяются
Удаление записи возвращает 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
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; поле локального состояния не объявлять синхронизированным до подтверждения. В текущие пакеты запись обратно не включена.
Возможность вызова конкретного метода не доказывает наличие прав на все записи. Перед живым импортом 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-часовую неделю)
Праздник, выпавший на выходной (09.05.2026; 01.05 и 09.05.2027), переносится на ближайший рабочий день по ст. 112 ТК.
Границы календаря 2026-01…2027-12 едины для API и UI:
первый месяц плана ограничить ≤ 2027-07;
месяцы заявок — ≤ 2027-12; число месяцев формы уменьшается у границы; первый месяц некоммерческого отчёта ≤ 2027-01 для полного периода до 12 месяцев;
на месяц без календаря api.js отвечает 400.
Принятая формула 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 не требуется
Сотрудники и проекты из fake/Bitrix в форме выбора: SphereClient совместимый задача №215, ProjectSource, staging/status, ID mapping, деактивация, ручной run, экран справочника/ошибок.
П1; Q-01/Q-07 для real adapter; до ответа только контракт фейка, MR не объявлять живой интеграцией
РП создаёт заявку и просматривает свою карточку: 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С не требуется
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.
Эксплуатация синка и восстановление: worker lease/fencing, расписание тех же run, статус/алерты, backup/restore runbook, нагрузочный профиль и финальная трассировка.
П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-v25213150 все 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 внешние порты
Только сложная таблица плана и ввод 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 печатаются при падении:
REQ-008/015: план до effectiveDate неизменен; release после даты=0; reduce не увеличивает ни один день; сумма часов разбиения месяца равна целому; days=0 не заменяется месячным FTE.
REQ-004: принятый набор назначений не превышает 1+допуск ни в один рабочий день; перестановка заявок/строк не меняет результат проверки уже фиксированного плана.
REQ-009/010: сумма доходов частей равна доходу целого до финального округления; взвешенная ставка при положительных весах находится между min/max; нулевые часы не влияют на среднее; неизвестная активная ставка делает результат неизвестным.
REQ-026/027: накопительная утилизация = сумма числителей / сумма знаменателей; V не входит в A; при 0 знаменателя нет Infinity/NaN; замена missing на 0 меняет полноту, а не часы известных строк.
REQ-031/035: повтор одного snapshot не меняет число сущностей и сумму A; новая версия записи заменяет вклад, deletion убирает только его; поломанную последнюю страницу нельзя активировать как полный снимок.
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: только ключевые сценарии
РП создаёт две строки → РМ назначает и соглашается → план обновлён, журнал содержит реального автора, другой РП не видит заявку.
РП предлагает замену с даты → план до решения прежний → РМ подтверждает превышение → дневной план/освобождение/история изменены один раз.
РМ открывает отчёт с неполным фактом/отпуском/больничным → прочерки → fake Time полный снимок и отпуск=больничный=0 → правильные итоги/CSV; РП видит только свою область.
Два браузерных контекста редактируют одну заявку → второй получает 409, его ввод сохранён; повтор первого commandId не дублирует решение.
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; этот список задаёт повторяемые сценарии проверки.
Числа приведены для календаря выше и принятой персональной нормы.
NEW-01: при 88 ч, R = 4000, C = 4300 → доход 352 000, расход 378 400, разница −26 400 (красная). R = C → 0 (зелёная). Неизвестная C («На подбор») → расход и разница «—».
NEW-02: колонка «Расход» после «Доход» в форме, рассмотрении и просмотре. Пример (Михаил, «отели», отсутствия 0): окт 176 ч → 756 800 ₽ (красный, доход 704 000), ноя 159 ч → 683 700 ₽ (красный), дек 88 ч → 378 400 ₽ (красный).
NEW-07: «Расход» между «Доход» и «Статус». Суммы в списке, карточке и сумма месяцев совпадают для всех заявок seed (тест). Для заявки-изменения — «—».
NEW-03 календарь: calc даёт помесячную таблицу 2026–2027 из блока 1 (24 значения в тесте), 1 FTE в ноябре 2026 = 159 ч, 2027-02-20 — рабочий день на 7 ч.
NEW-03 норма: H = Hкал − отпуск − больничный. Окт: отпуск 16 + больничный 8 → H = 152. Отпуск = Hкал → H = 0 и % «—». Сумма отпуска и больничного > Hкал отклоняется (400). Больничный вводится в «Типы проектов, факт и отпуска» рядом с отпуском, пустое поле удаляет запись, 0 сохраняется.
FR 62 / AC 61 в новой редакции: удалить отпуск сотрудника → его H, N и проценты по N «—», «норма по календарю» и явно введённые часы факта видны; ввести 0 → значения известны.
AC 59 в новой редакции: Hкал 176, отпуск 16 → H = N = 160: 0,5 FTE → P = 80 ч и 50 %; явно введённый A = 88 ч → 55 %. Колонок «без отпусков» нет. AC 60: 80 + 132 ч при N 160 + 176 → 63,1 %.
Словарь, примечание плана и README: grep -rn "будней × 8\|без праздников\|пока не учитываются" js README.md → пусто.
NEW-04: 4 верхние вкладки и 5 подвкладок «Отчётов». В перенесённых подвкладках работают их фильтры, кнопки шапки, раскрытие и режим РП.
NEW-05: заголовок Σ и значения меняются с единицей (ч / FTE·мес / %). Пример: 0,5 / 1 / 0,75 FTE → Σ 2,25; 388 ч / 512 ч нормы → 75,78125 %, не сумма процентов.
NEW-06: строка «Итог» = сумма строк сотрудников по месяцам и Σ, проекты не прибавляются повторно, не меняется при свёртке, следует поиску, фильтру свободного ресурса и области РП.
NEW-08: кольцо коммерческой плановой утилизации рядом с плановым, с теми же фильтрами. Октябрь (все 6 сотрудников, отсутствия заполнены: отпуск Анны 16, прочие 0): N = 1040; коммерческие часы 256 → 24,6 %.
NEW-09: кольцо фактической утилизации; при неполном факте «—» с пояснением; при полном A / N × 100; при N = 0 «—». В seed — один месяц с полным фактом.
Дефект вне NEW: «Распределение» и CSV показывают введённые ЦФО и подразделение (отдельный коммит).
npm test, npm run check и git diff --check проходят. Playwright для РМ и РП на desktop и 320–390 px без JS-ошибок.
service/docs/validation.md
Проверка документальной редакции дополнения
Дата: 13.10.2026. Эталон прототипа — 5213150, дерево и код прототипа не изменены этим пакетом. Проверяются документы, их расчётные примеры и публикация; общая QA всех NEW и риск-ревью ещё предстоят. Будущие тесты сервиса из testing/tasks не запускались: серверного кода здесь нет.
Локальные проверки
node scripts/verify-docs.mjs docs ../prototype-v2 ../analysis: 38 уникальных REQ, распределение в 11 будущих пакетов, 9 строк матрицы NEW, существование файлов реализации/тестов и внутренних Markdown-ссылок. Расчётная сверка 24 календарных норм, H=152/P=76, октября/ноября, 35,5 ч вывода, границы недель 8/4/0 и демо-денег — успешно. Проверено 46 внутренних Markdown-ссылок; это проверка существования целей, не оценка покрытия бизнес-поведения.
npm test в неизменном prototype-v2 — 44 теста прошли, 0 падений; npm run check успешен. Это локальная проверка принятой базы, не новая итоговая QA.
Все 11 Mermaid-блоков architecture/data-flows/spec извлечены и отрендерены через mermaid-cli с установленным headless Chromium. Обновлены SVG, Mermaid-исходники и HTML. F4 отражает обе записи отсутствий/coverage, схема хранения — AbsenceEntry и Coverage.
Хеши исходного v1.7, его изображений, старого design-sample.html/png и «Итераций» совпадают с сохранёнными до правок. Исторические analysis/plan, gap и аудит снабжены пометками приоритета без переписывания старых выводов.
Реальные API Сферы/Time/1С, БД, Docker, рабочие секреты не вызывались. Публичные ссылки источников сохранены; старые источники инфраструктуры относятся к проектируемому сервису.
Проверка публикации выполняется 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; окружение передаётся аргументами, новых переменных сервиса нет.
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 не закрыта этим документальным пакетом.