service/docs/CONTEXT.md
Словарь предметной области
Редакция 13.10.2026 по дополнению NEW-01–09 и реализации прототипа 5213150. Расчётное поведение ниже принято для прототипа; архитектура рабочего сервиса утверждена 15.10.2026, живые подключения ждут ответов open-questions.md. Общая 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, закрытие, связи РП. Имя — подпись, не ключ. |
| Тип объекта |
ProjectKind |
project/product/service — «проект/продукт/сервис» Контура; приходит из источника проектов. Независим от категории проекта; неизвестный тип — «Не указан». |
| Контур |
Contour |
Платформа сквозного процесса (лид → договор → проект → команда → бюджет → P&L → закрытие); сервис — её этап «Команда и ресурсы». ADR-0007. |
| Черновик заявки |
RequestDraft |
Заготовка заявки от внешнего клиента (план работ Контура): проект и необязательные строки. Не заявка: не проходит решение и не влияет на план. |
| Событие интеграции |
IntegrationEvent |
Запись outbox о решении, версии назначения, завершении или изменении факта; читается лентой по курсору, at-least-once, с версией схемы. |
| Политика решения |
DecisionPolicy |
Правило, чьи решения нужны, чтобы pending стала approved; по умолчанию — одно решение РМ. |
| Подразделение |
Department |
Внешний ID, имя, parentId; несколько принадлежностей сотрудника возможны, основное для отчёта — Q-07. |
| План месяца |
MonthPlan |
month YYYY-MM, fte и необязательные days; явный 0 в days важнее месячного значения. |
| Плановая загрузка |
plannedFte |
Доля полной загрузки, P/H; сумма для группы — FTE, не количество людей. |
| Фактическая загрузка |
actualFte |
Для ручного ввода 0…2; при импорте часов производное A/H; не равна плану по умолчанию. |
| Норма часов одного сотрудника H |
normHours |
Персональная H = Hкал − объединённые рабочие часы отпуска и больничного. Неизвестна, если нет подтверждения любого вида отсутствия. |
| Календарная норма Hкал |
calendarHours |
Сумма date→hours федерального производственного календаря. Отдельна от персональной H. |
| Больничный B |
sickLeaveHours |
Тот же формат и полнота, что отпуск. Общие даты учитываются один раз. |
| Полнота отсутствий |
AbsenceCoverage |
Для сотрудника×месяца нужны отпуск и больничный; пусто не равно 0. |
| Доступные часы дня |
availableDayHours |
Часы календаря минус объединённые датированные отсутствия; сводные часы распределяются пропорционально. |
| Плановые часы P |
plannedHours |
FTE×персональная H; при days — сумма дневного FTE×доступные часы дня. Отдельной нормы продажи нет. |
| Доходная ставка R |
incomeRate |
Положительные ₽/ч, одна на строку и период. |
| Расходная ставка C |
costRate |
₽/ч специалиста; при решении сохраняется costRateSnapshot, источник и версия. |
| Плановый доход I |
plannedIncome |
P×R, не подтверждённая выручка. |
| Плановый расход |
plannedCost |
P×C. |
| Превышение расходной ставки Δ |
costExcess |
max(C−R,0); не путать с incomeRate−costRate. |
| Средние ставки Rср/Cср |
weightedIncomeRate / weightedCostRate |
Взвешены плановыми часами, при нуле часов неизвестны. |
| Разница ставок / средних ставок |
rateDifference / weightedRateDifference |
R−C / Rср−Cср. |
| Разница плановых сумм |
plannedDifference |
plannedIncome−plannedCost. |
| Превышение планового расхода за период |
excessPlannedCost |
Δ×ΣP отдельной строки. |
| Фактические часы A |
actualHours |
Работы из Time или отдельно подтверждённого ручного факта; отпуск исключён. |
| Норма часов группы N |
groupNormHours |
Σ персональных H выбранных сотрудников/месяцев. Неполнота любой H делает N неизвестной. |
| Часы отпуска V |
vacationHours |
Employee×month, сводные рабочие часы 0…Hкал либо полные даты; независимо от проекта. Не часы работ. |
| Коммерческая плановая утилизация |
commercialUtilization |
Pком/N×100; те же люди и фильтры, только коммерческий числитель. |
| Сумма FTE за период |
periodFte |
Сумма месячных FTE, единица FTE·мес; процент периода — ΣP/ΣH. |
| Плановая/фактическая утилизация по норме |
plannedUtilization / actualUtilization |
P/N×100 и A/N×100, индивидуально знаменатель H. |
| Свободные часы / свободный ресурс |
freeHours / freeFte |
Σmax(H−P,0) / Σmax(1−plannedFte,0). |
| Доля отпуска |
vacationShare |
V/ΣHкал×100; при общей доле разных отсутствий использовать объединение, не сумму пересекающихся V и B. |
| Полнота факта |
ActualCoverage |
Покрытая область людей, проектов, дат и версия источника; отсутствие записи без полного покрытия неизвестно. |
| Журнал решений |
DecisionLog |
Неизменяемые автор, время, действие, before/after и версии; не технический лог приложения. |
| Снимок календаря |
CalendarVersion |
Версионированный набор date→hours; основание расчёта, не личный график сотрудника. |
| Сопоставление |
ExternalIdentity |
Уникальная тройка source + entityType + externalId → localId; email/название не ключ. |
Единицы и совместимость
Дата бизнеса — календарная дата Europe/Moscow; timestamp журнала — UTC instant. День изменения включён в новый период. Статус участия ended вычисляется по Clock, не хранится cron-обновлением.
Часы/FTE/ставки — точные десятичные значения; деньги — копейки или decimal. Формулы не округляют промежуточные результаты. Предложение: rates numeric(18,2), FTE numeric(12,8), hours numeric(20,8), денежный результат decimal с округлением до копейки при представлении; утверждение масштаба и округления — Q-11. JSON decimal передаётся строкой, домен не вычисляет деньги через JS float.
Неизвестно — null плюс причина missing/stale/unmapped, а не 0. В существующей месячной planSummary пустая выборка → 0%; кольца utilizationIndicators и процент периода при N=0 → неизвестно. Hкал остаётся видимой при неизвестной H. Подробнее — calculation-contract.md.
Адаптация прототипа: RequestItem → Request; Line → ResourceLine; targetId+lineIndex → assignmentId с таблицей соответствий при импорте; имена project/manager → projectId/managerId; months[].rate → взвешенная incomeRate. days сохраняется. Демо seed.js остаётся источником сценариев, не справочником рабочего сервиса.
Словарь будущего сервиса использует стабильные ID и Decimal; прототип остаётся на JS Number и именах проектов, localStorage/revision. Эти проектируемые изменения не реализованы документальной редакцией.
service/docs/addendum-coverage.md
Прослеживаемость NEW-01–09
Редакция по дополнению 13.10.2026 и фактическому prototype-v2 5213150. Все девять реализованы принятыми пакетами; эта матрица связывает требования с существующими файлами/проверками, но не объявляет общую QA и риск-ревью завершёнными. Исходный v1.7 и старые картинки сохранены как история. Расчётный контракт, спецификация, проверки, будущий сервис.
Все пути к реализации и тестам ниже — относительно prototype-v2, документы — относительно service/docs. Unit — Node npm test; browser-файлы — функции для Playwright на опубликованной странице. Проектируемые L1–L5/HTTP-интеграции сервиса не считаются этими тестами выполненными.
| NEW |
Принятое поведение / REQ |
Код и владелец |
Проверки существующего прототипа |
Документ |
| NEW-01 |
Доход/расход/разница периода; отрицательная красная, ≥0 зелёная, неизвестная C прочерк; REQ-009 |
js/calc.js requestFinance; js/views/request-modals.js financialTotal |
tests/addendum-ui.test.js, tests/personal-norm.test.js, tests/browser-addendum.js, tests/browser-personal-norm.js; 88 ч →352000/378400/−26400, 0, неизвестность, личная H |
spec, calculation-contract: P и деньги; analysis/design-base §7 |
| NEW-02 |
Расход после Дохода в месячных таблицах формы/рассмотрения/просмотра; отрицательная разница выделяет расход; REQ-009 |
js/views/request-modals.js lineCard → calc.requestFinance |
tests/addendum-ui.test.js, tests/browser-addendum.js; сумма месячных строк=периоду |
spec, calculation-contract; analysis/design-base §7 |
| NEW-03 |
Производственный календарь, H сотрудника, полнота отпуск/больничный, union дат, P=FTE×H, N=ΣH; REQ-008/024/026/027 |
js/calc.js, js/api.js, js/seed.js, js/ui.js, js/views/data-modals.js; потребители плана/отчётов/денег/изменений |
tests/personal-norm.test.js, tests/data.test.js, tests/browser-personal-norm.js, tests/browser-calendar-boundary.js; 24 нормы, H=152/0/null, суммы/атомарность/рабочая суббота/январь–март |
spec, CONTEXT, calculation-contract, architecture, data-flows F4, ADR-0006, Q-06 |
| NEW-04 |
Четыре верхние вкладки и пять разделов Отчётов; собственное состояние/действия сохранены; REQ-018/022/023 |
js/ui.js, js/app.js, js/views/index.js, js/views/reports.js |
tests/navigation.test.js, tests/addendum-ui.test.js, tests/browser-addendum.js; переключения, РМ/РП, фильтры, кнопки |
spec REQ-018, analysis/design-base §3; calculation-contract |
| NEW-05 |
Σ меняет единицу; FTE·мес суммирует месячные FTE, % взвешен нормами; REQ-011/026 |
js/calc.js periodTotal/planPeriod; js/views/plan.js |
tests/aggregates.test.js, tests/browser-aggregates.js; 2,25, 388/512, unknown/zero |
spec, CONTEXT, calculation-contract; analysis/design-base §4 |
| NEW-06 |
Нижний Итог без повторного учёта проектов, неизменный при свёртке, по фильтрам/РП; REQ-011/026 |
js/calc.js planTotal; js/views/plan.js |
tests/aggregates.test.js, tests/browser-aggregates.js; поиск имени/проекта/свободного ресурса/scope, collapse |
spec, calculation-contract; analysis/design-base §4 |
| NEW-07 |
Расход после Дохода в списке, отрицательная разница красная, неизвестные суммы прочерк; REQ-009 |
js/views/requests.js, js/views/shared.js → calc.requestFinance |
tests/addendum-ui.test.js, tests/browser-addendum.js, tests/browser-personal-norm.js; список=карточка, личная H |
spec, calculation-contract; analysis/design-base §7 |
| NEW-08 |
Коммерческое плановое кольцо с общей группой/нормой, ограничен числитель; REQ-018 |
js/calc.js utilizationIndicators; js/views/reports.js, styles.css |
tests/aggregates.test.js, tests/browser-aggregates.js; 256/1040, фильтры, неизвестная/нулевая H |
spec, calculation-contract; analysis/design-base §8 |
| NEW-09 |
Фактическое кольцо A/N, неполнота/нулевая/неизвестная N → прочерк с причиной; REQ-018/027 |
js/calc.js actualHours/reportPeriod/utilizationIndicators; js/views/reports.js; seed полного ноября |
tests/aggregates.test.js, tests/browser-aggregates.js, tests/personal-norm.test.js; 477/954=50%, partial/null/zero |
spec, calculation-contract, testing; analysis/design-base §8 |
Отдельный принятый старый дефект: ЦФО/подразделение и CSV — js/views/reports.js, tests/distribution.test.js, tests/browser-addendum.js, REQ-020. Это не десятое требование дополнения.
Следующая общая QA
Повторить все связанные browser-сценарии на одном опубликованном состоянии, РМ/РП и 1440/320/375/390 px; сверить все NEW, отсутствие JS-ошибок, скролл таблиц, полноту/нули и денежные представления. Результаты прежних пакетов и количество unit-тестов не подменяют эту приёмку. Документы service описывают будущие подключения и авторизацию, а не готовый рабочий сервис.
service/docs/adr/0001-data-master.md
ADR-0001: Мастер заявок и назначений
Статус: утверждено 15.10.2026 (Евгений); предложено 13.10.2026. Живые подключения — после ответов на относящиеся 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 не выбран без реестра и утверждённой модели.
Дополнение 15.10.2026
Мастер справочника проектов может смениться со Сферы на Контур (ADR-0007): это смена адаптера ProjectCatalog и ExternalIdentity, а не мастера заявок и назначений. Мастером Request/Assignment/DecisionLog остаётся сервис; Контур получает их только лентой событий, без записи в его таблицы.
service/docs/adr/0002-sync-worker.md
ADR-0002: Явный синк и отдельный worker
Статус: утверждено 15.10.2026 (Евгений); предложено 13.10.2026. Живые подключения — после ответов на относящиеся Q.
Контекст
INT 02/04 требуют повторяемого синка и сохранения истории при сбоях. задача №215 задаёт SphereClient и ручной/15-минутный запуск. Nitro tasks остаются experimental.
Решение
Один модуль DirectorySync.run для теста/ручного вызова/worker. Production: отдельный процесс того же Node-релиза (NestFactory.createApplicationContext, без HTTP; ADR-0005), задания/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: Серверные секреты интеграций
Статус: утверждено 15.10.2026 (Евгений); предложено 13.10.2026. Живые подключения — после ответов на относящиеся 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: Идентификация отдельно от прав
Статус: утверждено 15.10.2026 (Евгений); предложено 13.10.2026. Живые подключения — после ответов на относящиеся 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.
Дополнение 15.10.2026
Третий IdentityProvider — доверенный шлюз платформы Контур (ADR-0007): личность из подписанного заголовка или токена, проверка подписи на сервере. Роли платформы переводятся в AccessGrant версионируемой RoleMapping (Q-14); роль из запроса клиента, как и раньше, не доверяется.
service/docs/adr/0005-stack-modules.md
ADR-0005: Стек и глубокие модули
Статус: утверждено 15.10.2026 (Евгений); предложено 13.10.2026, пересмотрено 15.10.2026 (UI — микрофронтенд на Vue + Vite вместо Nuxt; API — NestJS вместо Nitro).
Контекст
Нужна ручная поддержка и близость к задача №215 (Nuxt 4/Nitro, Drizzle, zod); UI прототипа — vanilla mock, ТЗ описывает ещё более раннюю Cloudflare-версию. Сервис делается standalone, но потом встраивается в платформу Контур как микрофронтенд (ADR-0007); механизм встраивания и стек хоста не известны (Q-12). Компонент Nuxt живёт только внутри Nuxt-приложения — useNuxtApp, роутер, плагины, runtimeConfig и автоимпорты в чужом хосте отсутствуют; SSR внутреннему инструменту не нужен. Для API нужен фреймворк, на котором сервис смогут поддерживать бэкендеры со стороны; DI нужен самой архитектуре — порты с fake и живыми адаптерами выбираются в composition root.
Решение
TypeScript strict, Node 22 LTS, pnpm-монорепо, zod/shared DTO, Postgres/Drizzle, PGlite локально/тесты, Vitest/fast-check/Playwright. Шесть глубоких модулей из architecture.md, чистый Calculations и заменяемые ports.
- API — NestJS 12 (ESM, платформа Express). Глубокий модуль architecture.md = Nest-модуль, наружу экспортирует только свой фасад; порты — injection tokens, адаптеры fake/живые выбирает AppModule по конфигурации. Контроллеры тонкие: zod-DTO через
nestjs-zod (createZodDto, глобальный ZodValidationPipe), OpenAPI — @nestjs/swagger + cleanupOpenApiDoc из тех же zod-схем; class-validator не используется. Access — guard, создающий ActorContext; DomainError → общий формат ошибки — exception filter. Конфигурация — @nestjs/config с проверкой env схемой zod.
- Домен без Nest.
domain/ и packages/calculations — чистый TypeScript без декораторов и @Injectable; Nest-провайдеры оборачивают их через useFactory в server/. Декораторы живут только в server/.
- Worker — тот же релиз, отдельный процесс
NestFactory.createApplicationContext(WorkerModule) без HTTP (ADR-0002); расписание только создаёт задания в Postgres.
- UI — Vue 3 + Vite + vue-router, SPA без SSR, собирается как микрофронтенд. Единственный вход —
mount(el, ctx), возвращающий unmount(); ctx даёт apiBase, способ авторизации запросов, режим роутера (history с basePath или memory), язык и обработчик навигации наружу. Каждое монтирование создаёт свой экземпляр Vue-приложения, роутера и состояния; глобальных переменных, слушателей вне корня и стилей вне корня нет.
- Standalone — тонкая оболочка над тем же
mount: шапка, вход через Access, history-роутер. Отдельной сборки экранов для standalone нет.
- Встраивание — Module Federation (решено 15.10.2026).
ui/ собирается remote через @module-federation/vite: remoteEntry.js отдаёт модуль ./mount; Vue объявлен shared singleton — если хост на Vue, берётся его копия, иначе своя из сборки. Standalone-оболочка грузит тот же mount напрямую. Стек хоста (Q-12) меняет только настройки shared, домен, API и экраны не меняются.
Последствия
Один язык и репозиторий; перенос смысла calc.js без seed globals; стандартный для рынка Node-бэкенд, понятный без знания Nuxt; Postgres concurrency тесты отдельно от embedded БД. Без Nuxt нет автоимпортов, file-based routing и useFetch: роуты, API-клиент и обработка ошибок описываются явно — это плата за переносимость. Стили изолируются корневым классом и @layer; шрифты AeonikPro коммерческие и подключаются только оболочкой, а не микрофронтендом. Vitest не выпускает метаданные декораторов через esbuild: тестам Nest нужен SWC-плагин (unplugin-swc) — настраивается в П1. Nest 12 — только ESM: зависимости и конфиги проверяются на ESM в П1. Патч-версии фиксировать в П1 с lockfile.
Отвергнутые варианты
Nuxt 4 UI+Nitro (редакция 13.10.2026) — удобен standalone, но не встраивается в хост на другом фреймворке без iframe. Nitro без Nuxt (промежуточная редакция 15.10.2026) — легче, ближе к задача №215, но ниша: порты и composition root пришлось бы собирать руками, а поддержку искать среди знающих Nuxt. Fastify-платформа Nest — быстрее, но меньше готовых middleware для cookie/CSRF; переход возможен без изменения модулей. Web component и iframe как механизм встраивания — отвергнуты решением 15.10.2026 в пользу Module Federation; iframe остаётся только запасным вариантом отладки. Микросервисы усложнят транзакции и диагностику; generic слои репозиториев раздувают интерфейсы; повтор Cloudflare D1/State main и переключателя ролей не соответствует рабочему контуру.
service/docs/adr/0006-versioned-data.md
ADR-0006: Снимки решений, календаря и факта
Статус: утверждено 15.10.2026 (Евгений); предложено 13.10.2026. Живые подключения — после ответов на относящиеся Q.
Контекст
NFR 02 требует исходных и согласованных значений. Справочники/календари меняются, Time исправляет часы; повторная сумма приводит к дрейфу отчётов.
Решение
Формулы дополнения приняты: H = календарь минус объединённые рабочие часы отпуска/больничного, P=FTE×H, N=ΣH; отдельной нормы продажи нет. Расчётный контракт фиксирует полноту и допущения actuals/отсутствий прототипа 5213150.
Рабочая архитектура (утверждена 15.10.2026): исходный 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/adr/0007-contour-context.md
ADR-0007: Сервис как ограниченный контекст Контура
Статус: утверждено 15.10.2026 (Евгений). Интеграция с Контуром отложена; сейчас реализуются только заделы — API-first, ProjectCatalog и тип объекта (REQ-039, REQ-040, REQ-047). Остальное решение — ориентир границы, а не работа текущих пакетов.
Контекст
Ресурсное планирование станет этапом 04 «Команда и ресурсы» платформы Контур (прототип процесса: https://commercial-resource-contour.jammyglade1.chatgpt.site/). Контур ведёт сквозной процесс лид → сделка → договор → проект/продукт/сервис → план работ → команда → бюджет → трекинг → P&L → изменения → приёмка → закрытие и восемь ролей. Карточку проекта создаёт договор, а не Сфера; заявку на ресурс открывают из карточки проекта; бюджет берёт плановые FTE из согласованных назначений, фактические — из внесённых часов; изменения согласуют несколько ролей; после приёмки руководитель практики освобождает команду. Стек и способ встраивания Контура не определены (Q-12).
Решение
Сервис остаётся самостоятельным модульным монолитом (ADR-0005), но его граница с платформой задаётся портами и API, а не общими таблицами или UI:
- API-first. Всё, что делает UI, доступно через
/api/v1 с OpenAPI, сгенерированным из zod-контрактов. domain/ и server/ не импортируют ui/ и Vue. UI — микрофронтенд с mount(el, ctx) (ADR-0005): Контур монтирует те же экраны, что и standalone-оболочка. Calculations публикуется отдельным пакетом без зависимостей от Vue, Nitro, БД и DOM.
- ProjectCatalog вместо прямой зависимости от Сферы: адаптер Сферы сейчас, адаптер Контура потом. Проект несёт
kind = project|product|service и внешнюю ссылку; локальный projectId не меняется при смене источника.
- ActualsSource вместо TimeSource в домене: Time сейчас, трекинг Контура потом; правила полноты и идемпотентности одни.
- Права от платформы. Access принимает проверенную личность от IdentityProvider: Bitrix OAuth или доверенный шлюз платформы. Роли Контура сопоставляются с AccessGrant конфигурацией (Q-14); домен знает только pm, rm, viewCost и read-only scope отчётов.
- DecisionPolicy. Решение по заявке и изменению проходит через политику: по умолчанию достаточно решения РМ; платформа может потребовать дополнительные согласования. До полного набора решений план не меняется — это уже инвариант pending (REQ-016).
- Outbox. Факты для бюджета, P&L и закрытия публикуются событиями в той же транзакции, что и решение; потребители читают ленту
GET /api/v1/events по курсору. Брокер не вводится.
- Внешние команды. Платформа может создать черновик заявки по проекту и инициировать освобождение команды; решение по-прежнему принимает РМ.
Последствия
Заделы П1 и П4 почти ничего не стоят. Outbox, ленту событий и DecisionPolicy сейчас не делаем: при запуске Контура их добавление затронет транзакции решений П6 и П8 — это осознанная цена отсрочки. Цена — лента событий с версиями схемы, contract-тесты на OpenAPI и события, конфигурация сопоставления ролей. UI встраивается в Контур через Module Federation (ADR-0005): Контур грузит ./mount из remoteEntry.js, домен и экраны от этого не зависят.
Отвергнутые варианты
Общая БД с Контуром ломает владение транзакциями и блокировками ёмкости (ADR-0001). Брокер сообщений сейчас не оправдан при одном потребителе и неизвестном стеке. Nuxt для UI отвергнут в ADR-0005: его компоненты не живут вне Nuxt-приложения. Перенос бюджета, P&L и многосторонних согласований внутрь сервиса раздувает контекст: это модули Контура, сервис только отдаёт им факты.
service/docs/architecture.md
Архитектура рабочего сервиса
Редакция 13.10.2026 по дополнению NEW-01–09 и реализации прототипа 5213150. Расчётное поведение ниже принято для прототипа; архитектура рабочего сервиса утверждена 15.10.2026, живые подключения ждут ответов open-questions.md. Общая QA всех NEW ещё предстоит. Расчётный контракт, матрица покрытия.
Статус: утверждено 15.10.2026 (Евгений); живые подключения — после ответов на относящиеся вопросы open-questions.md. Требования — 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 (здесь UI — Vue + Vite-микрофронтенд без Nuxt, API — NestJS, ADR-0005), 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
K[Контур: проекты, бюджет, P&L, закрытие] -.->|Проекты, черновики заявок, освобождение команды| S
S -.->|Лента событий решений и назначений| K
Пунктир — граница с платформой Контур по ADR-0007: до её запуска проекты идут из Сферы, ленту событий никто не читает.
Пользователи не обращаются к интеграционным API напрямую. OAuth обмен и служебный REST-синк — разные подключения. Time не считается стандартным методом Bitrix24. Ни один поток не пишет в Сферу (рекомендация по INT 03).
C4 L2: контейнеры
flowchart TB
U[Браузер: standalone-оболочка или хост → микрофронтенд Vue] -->|HTTPS, cookie, общие DTO| A[NestJS API: доступ и модули]
A -->|Транзакции и проекции| D[(Postgres)]
W[Фоновый worker: Nest application context, те же модули] -->|Задания, lease, checkpoint| D
A -->|Ручной запуск: запись задания| D
W -->|Порты чтения| E[Bitrix24 / Time / 1С]
A -->|Порт идентификации| I[OAuth / SSO]
A --> L[Логи, метрики, Sentry]
W --> L
D --> B[Резервные копии и проверка восстановления]
Один репозиторий и один модульный монолит. В production API и worker — два процесса одного релиза с Postgres. Worker выбран вместо experimental Nitro scheduledTasks (ADR-0002); Redis и брокер сообщений не нужны. Scheduler создаёт задания в БД, worker обрабатывает их с lease и повтором после падения. При локальной PGlite только один владелец БД — API-процесс со встроенным ручным runner, без второго процесса и cron; отдельный worker требует DATABASE_URL. Это предотвращает совместный доступ процессов к embedded-файлу.
C4 L3: глубокие модули
flowchart LR
H[HTTP handlers] --> A[Доступ]
H --> R[Заявки и согласование]
H --> P[Назначения и ресурсный план]
H --> V[Отчёты]
R --> P
R --> C[Расчёты]
P --> C
V --> C
R --> A
P --> A
V --> A
W[Worker / ручной запуск] --> S[Справочники и синк]
S --> X[SphereClient / ProjectSource / TimeSource / OneCSource]
R --> DB[(Транзакционная БД)]
P --> DB
V --> DB
S --> DB
A --> DB
Модули сами владеют запросами к своим таблицам, а не цепочкой generic repository/service/controller. Общий UnitOfWork — инфраструктурная граница транзакции, не отдельный бизнес-модуль. Доступ обеспечивает policy-функции и scoped query predicates; ни один публичный метод чтения не принимает произвольный SQL scope от клиента.
Сигнатуры ниже — эскиз контракта TypeScript, не код сервиса. Result<T> — дискриминированный результат {ok:true,value:T} либо {ok:false,error:DomainError}; Promise только для I/O. DTO определяются один раз в shared/contracts/, схемы zod выводят типы. ActorContext создаётся сервером через Доступ. ID — номинальные строковые типы; Decimal — точное число, сериализуемое строкой. Tx выдаётся только UnitOfWork; наружу HTTP не выходит.
Заявки и согласование — Requests
interface Requests {
create(actor: ActorContext, cmd: CreateRequest): Promise<Result<RequestView>>;
proposeChange(actor: ActorContext, cmd: ChangeAssignment): Promise<Result<RequestView>>;
decide(actor: ActorContext, cmd: DecisionCommand): Promise<Result<RequestView>>;
get(actor: ActorContext, id: RequestId): Promise<Result<RequestView>>;
list(actor: ActorContext, query: RequestQuery): Promise<Result<Page<RequestView>>>;
}
// Команды содержат expectedVersion и commandId; DecisionCommand — approve/return/reject.
Прячет: конечный автомат, validation ролей/ставок, idempotency, проверку версии, снимки первичной заявки, журнал решений и одну транзакцию изменения назначений. Вызывает: контроллеры API. Проверяется: L1 автомат и правила решения; L2 через create/decide с настоящей БД; L4 права и ошибки, L5 заявка→план. Requests владеет Request/ResourceLine/AssignmentChange/DecisionLog/CommandReceipt. Вызов Assignments.applyDecision происходит внутри его Tx, а не отдельным HTTP-запросом.
Назначения и ресурсный план — Assignments
interface Assignments {
previewChange(actor: ActorContext, cmd: ChangeDraft): Promise<Result<ChangePreview>>;
getPlan(actor: ActorContext, query: PlanQuery): Promise<Result<PlanView>>;
list(actor: ActorContext, query: AssignmentQuery): Promise<Result<Page<AssignmentView>>>;
applyDecision(tx: Tx, decision: ApprovedDecision): Promise<Result<AssignmentVersion[]>>;
}
Прячет: дневные интервалы, состояния active/ended, единственное ожидающее изменение, блокировки общей ёмкости людей, версии назначений и согласованную проекцию плана. Вызывает: API для чтения/превью, Requests для решения; ApprovedDecision — внутренний тип модуля, клиент его не создаёт. Проверяется: L1 дневные преобразования; L2 смена версии без двойного учёта и откат; Postgres-проверка конкурентного решения; L4 область РП; L5 замена. Владеет Assignment/AssignmentVersion/AssignmentDay; изменение существующей версии запрещено, текущий указатель меняется транзакционно.
Расчёты — Calculations
interface Calculations {
employeeNorm(calendar: CalendarMonth, absences: EmployeeMonthAbsences): Decimal | null;
plannedHours(plan: MonthPlan, availability: EmployeeAvailability): Decimal | null;
changePlan(plan: AssignmentPlan, change: ChangeDraft, availability: EmployeeAvailability): ChangePreview;
overloads(plans: AssignmentPlan[], availability: AvailabilityByEmployee): Overload[];
finance(items: FinancialItem[]): FinanceTotals;
utilization(periods: PeriodTotals[]): UtilizationTotals;
releaseDate(plan: AssignmentPlan, availability: EmployeeAvailability): LocalDate | null;
periodTotal(periods: PersonalPeriod[]): PeriodTotals;
planTotal(rows: VisibleEmployeePlan[]): PlanTotals;
utilizationIndicators(input: ScopedUtilizationInput): UtilizationIndicators;
}
Прячет: формулы §1, приоритет days, взвешивание, допуски 0,00001 FTE / 0,001 п.п., отсутствие данных и правила точности. Вызывают: Requests/Assignments/Reports. Проверяется: L1 таблицы AC и property-based fast-check, без БД/сети. База — функции calc.js; глобальные imports seed.js из costRate/viewScope заменяются аргументами. Clock нужен вызывающему модулю для проверки даты, Calculations получает уже определённую дату. Никаких Date.now(), fetch или process.env.
Расчётные входы и полнота
Контракт calculation-contract.md описывает реализованные значения. Для домена будущего сервиса EmployeeAvailability содержит календарь, обе записи отсутствий и их полноту; это не второй тип нормы продажи. PersonalPeriod содержит персональные hours/norm, VisibleEmployeePlan — уже ограниченный состав людей, ScopedUtilizationInput — один scope для P, Pком и A. Null сохраняется до представления. Отсутствия объединяются до расчёта доступных часов; UI не повторяет вычет. Сводные часы без дат распределяются пропорционально календарным часам только как допущение прототипа; рабочий источник и точность дат — Q-06.
Предложение хранения: AbsenceEntry(employeeId, kind=vacation|sick, month, dates или hours, sourceVersion), AbsenceCoverage(employeeId, month, kind, confirmed). Пропуск записи без coverage не является нулём. В прототипе coverage выражено наличием записи в reportData, а не отдельной таблицей. Импорт, версии БД и HTTP-порт отсутствий ещё не реализованы.
Отчёты — Reports
interface Reports {
get(actor: ActorContext, query: ReportQuery): Promise<Result<ReportView>>;
exportCsv(actor: ActorContext, query: ExportQuery): Promise<Result<CsvFile>>;
updateLocalData(actor: ActorContext, cmd: LocalReportDataCommand): Promise<Result<ReportDataVersion>>;
}
Прячет: безопасную область и агрегации, категории проектов, actual coverage, отсутствие факта, текущие кадровые атрибуты, проекцию ставок по viewCost, экранирование CSV. Вызывают API и UI через API. Проверяется: L1 примеры AC 41–66 в Calculations; L2 real DB фильтры/полнота; L4 матрица прав и отсутствие утечек в CSV; L5 отчёт. Владеет только локальными overrides/категориями/ручными actuals/vacations/sickLeaves; импортированные snapshots принадлежат Sync и читаются, не редактируются. Итоги собираются одной согласованной версией снимков, версии возвращаются в метаданных.
Справочники и синк — DirectorySync
interface DirectorySync {
enqueue(actor: OperatorContext, source: SourceKind): Promise<Result<SyncRunId>>;
run(input: SyncInput): Promise<Result<SyncResult>>;
status(actor: ActorContext, source: SourceKind): Promise<Result<SyncStatus>>;
resolveIdentity(actor: OperatorContext, cmd: IdentityMapping): Promise<Result<MappingVersion>>;
}
// SyncInput: source, runId, asOf, mode=incremental|reconcile; часы/ID внедряются фабрикой.
Прячет: постраничное чтение, staging, checkpoint, дедупликацию, полные сверки, мягкую деактивацию, карантин неподтверждённых сопоставлений и атомарную активацию снимка. Вызывают worker и защищённый операторский API; run доступен программно без scheduler. Проверяется L2 повтор, обрыв, восстановление; L3 адаптеры на фикстурах; L4 только оператор запускает; L5 устаревание. Владеет справочниками, ExternalIdentity, SyncRun/SyncCheckpoint, ImportSnapshot, ActualEntry/Coverage, CalendarVersion/Day, AbsenceSnapshot/Coverage, CostRateVersion. Детерминизм: одинаковые входы+Clock+IdGenerator → одинаковые изменения доменных данных; технический runId может отличаться.
Доступ — Access
interface Access {
beginLogin(input: LoginStart): Promise<Result<LoginRedirect>>;
finishLogin(input: LoginCallback): Promise<Result<SessionCookie>>;
authenticate(input: SessionInput): Promise<Result<ActorContext>>;
authorize(actor: ActorContext, operation: Operation, object: ObjectScope): Result<Permit>;
scope(actor: ActorContext, resource: ResourceKind): AuthorizedScope;
revoke(principalId: PrincipalId): Promise<void>;
}
Прячет: OAuth/OIDC flow, связь external subject→principal, сессии, права и scope, отзыв после деактивации. Вызывают все публичные модули и handlers. Проверяется L1 policy, L2 session/grants, L3 IdentityProvider, L4 полная матрица и production-negative fake-login, L5 вход в тесте. Владеет Principal, Session, AccessGrant; AccessGrant не редактируется Requests. ActorContext нельзя передать через JSON как доверенный объект.
Порты и адаптеры
interface SphereClient { // совместимо по именам и смыслу со задача №215
getTasks(ids: string[]): Promise<SphereTask[]>;
listGroupTasks(groupId: string, options: {openOnly: boolean}): Promise<SphereTask[]>;
searchGroups(q: string): Promise<SphereGroup[]>;
getGroupMembers(groupId: string): Promise<SphereUser[]>;
listDepartments(): Promise<SphereDepartment[]>;
searchUsers(query: {q?: string; departmentId?: string; ids?: string[]}): Promise<SphereUser[]>;
}
interface ProjectSource { read(cursor?: string): Promise<SourcePage<SourceProject>> }
interface TimeSource { read(query: TimeWindow, cursor?: string): Promise<SourcePage<TimeEntry>> }
interface OneCSource {
calendar(year: number): Promise<SourceSnapshot<CalendarDay>>;
absences(query: AbsenceWindow, cursor?: string): Promise<SourcePage<SourceAbsence>>;
costRates(cursor?: string): Promise<SourcePage<SourceCostRate>>;
}
interface IdentityProvider { begin(input: LoginStart): Promise<LoginRedirect>; verify(input: LoginCallback): Promise<VerifiedIdentity> }
interface Clock { now(): Instant; today(zone: 'Europe/Moscow'): LocalDate }
interface IdGenerator { next(): string }
interface CurrentUser { get(): Promise<Result<ActorContext>> }
SphereUser: id/name/position/departmentIds/active; SphereDepartment: id/name/parentId; SphereTask: id/title/stageName/closed/groupId/responsibleId/deadline; SphereGroup: id/name/membersCount. Методы, не нужные стаффингу сейчас, не вызываются ради заполнения данных. Поскольку searchUsers по умолчанию ищет только активных, сверка сохранённых ID выполняется явно через ids с получением ACTIVE=false; отсутствие не доказывает увольнение.
SourcePage содержит items/nextCursor/completeness/sourceVersion; общий транспорт не притворяется, что пагинация у всех систем одинаковая. ProjectSource реализуется GroupProjectSource, DealProjectSource или SmartProcessProjectSource, без проверки типа сущности внутри домена. TimeSource и OneCSource — проектируемые контракты: фактические endpoint и схема ещё не известны. Для каждого — HTTP-адаптер и Fake-адаптер. В e2e фейковые HTTP-серверы проверяют ещё и транспорт; модульные тесты используют in-memory fake порты.
Граница с Контуром
Сервис — ограниченный контекст «Команда и ресурсы» платформы Контур (ADR-0007); требования — REQ-039–048. Сейчас реализуются только ProjectCatalog с типом объекта и API-first; DecisionPolicy, Outbox, лента событий, шлюз платформы и ActualsSource описаны на будущее и отложены до запуска Контура.
interface ProjectCatalog { // обобщает ProjectSource; адаптеры: Сфера сейчас, Контур потом
read(cursor?: string): Promise<SourcePage<SourceProject>>; // SourceProject: externalId, name, kind, status, managerIds, contractRef?
}
interface ActualsSource { read(query: TimeWindow, cursor?: string): Promise<SourcePage<ActualEntry>> } // Time или трекинг Контура
interface DecisionPolicy { required(subject: DecisionSubject): DecisionRequirement[] } // по умолчанию [{role:'rm'}]
interface Outbox { append(tx: Tx, events: IntegrationEvent[]): Promise<void> } // только внутри транзакции решения
interface RoleMapping { grants(identity: VerifiedIdentity, platformRoles: string[]): AccessGrant[] } // версионируемая конфигурация, Q-14
ProjectSource из «Портов и адаптеров» становится реализацией ProjectCatalog; kind хранится колонкой Project. Переключение источника меняет только ExternalIdentity (REQ-040).
TimeSource — адаптер ActualsSource. Источник факта задаётся на период (REQ-046), Reports не знает адаптера.
- Requests.decide спрашивает DecisionPolicy; пока требования не выполнены, решение пишется частичным в DecisionLog, Assignments.applyDecision не вызывается (REQ-045).
- Requests и Assignments пишут IntegrationEvent через Outbox в своей Tx (REQ-042). Таблица
integration_event(seq bigserial, eventId, type, schemaVersion, aggregateId, aggregateVersion, payload, createdAt); лента читается по seq, payload C маскируется для потребителя без viewCost.
- Access получает второй IdentityProvider —
PlatformGatewayIdentity (подписанный заголовок или токен шлюза, проверка на сервере) и RoleMapping (REQ-044). Bitrix OAuth остаётся первым адаптером.
- UI — микрофронтенд с
mount(el, ctx) (REQ-049): Контур монтирует те же экраны, что и standalone-оболочка; правило импортов (REQ-047) держит домен независимым от UI.
Модель хранения и транзакции
erDiagram
REQUEST ||--|{ RESOURCE_LINE : contains
REQUEST ||--o{ DECISION_LOG : records
RESOURCE_LINE ||--o{ ASSIGNMENT : creates
ASSIGNMENT ||--|{ ASSIGNMENT_VERSION : versions
ASSIGNMENT ||--o{ ASSIGNMENT_CHANGE : changes
EMPLOYEE ||--o{ ASSIGNMENT : assigned
PROJECT ||--o{ REQUEST : requested
PROJECT ||--o{ PROJECT_MANAGER : grants
EMPLOYEE ||--o{ ACTUAL_ENTRY : worked
PROJECT ||--o{ ACTUAL_ENTRY : measured
CALENDAR_VERSION ||--|{ CALENDAR_DAY : defines
EMPLOYEE ||--o{ ABSENCE_ENTRY : absent
ABSENCE_SNAPSHOT ||--o{ ABSENCE_ENTRY : contains
ABSENCE_SNAPSHOT ||--o{ ABSENCE_COVERAGE : confirms
Таблицы нормализованы: нет одной JSON-записи State/main, которая блокирует весь сервис. JSON допустим для неизменяемого before/after в DecisionLog и валидированного сырого staging с ограниченным сроком хранения; фильтруемые поля и внешние ключи — колонки.
- Уникальность
(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/requests; GET /api/v1/requests/:id |
Requests.list/get |
200 scoped Page/RequestView |
| POST /api/v1/requests |
Requests.create |
201 RequestView; commandId; projectId/lines/comment |
| POST /api/v1/requests/:id/decisions |
Requests.decide |
200; expectedVersion, commandId, action, lines только для первичного approve, comment, allowLoss, expectedRateVersions |
| GET /api/v1/assignments; GET /api/v1/plan |
Assignments.list/getPlan |
200; period/filter/cursor |
| POST /api/v1/assignments/:id/preview |
Assignments.previewChange |
200; изменение без записи |
| POST /api/v1/assignments/:id/changes |
Requests.proposeChange |
201; expectedAssignmentVersion, mode/date/FTE/employeeId/reason |
| GET /api/v1/reports/:kind; GET /api/v1/reports/:kind.csv |
Reports |
200 JSON/CSV; только известные kind, одна область |
| PATCH /api/v1/report-data |
Reports.updateLocalData |
200; expectedVersion, commandId; только локальные поля |
| GET /api/v1/sync/status; POST /api/v1/sync/:source |
DirectorySync |
200 статус для РМ; 202 runId оператору запуска |
| GET /auth/sphere; GET /auth/sphere/callback; POST /auth/logout |
Access |
redirect/cookie; logout отозвает сессию |
| POST /api/v1/request-drafts |
Requests.createDraft, отложено до Контура |
201 {draftId, url}; доверенный клиент платформы, projectId и необязательные строки; план не меняется (REQ-041) |
| POST /api/v1/projects/:id/release |
Requests.proposeRelease, отложено до Контура |
201 список созданных pending-изменений и пропущенных назначений; commandId, effectiveDate, reason (REQ-043) |
| GET /api/v1/events |
Outbox, отложено до Контура |
200 {items, nextCursor}; after, limit; потребитель платформы (REQ-042) |
| GET /api/v1/openapi.json |
Инфраструктура |
200 схема v1 из shared/contracts (REQ-047) |
| GET /api/health/live; GET /api/health/ready |
Инфраструктура |
200 live; ready 503 при недоступной БД/несовместимой схеме |
Общая ошибка {error:{code,message,details?},requestId}: 400 INVALID_REQUEST; 401 UNAUTHORIZED; 403 FORBIDDEN; 404 NOT_FOUND для чужого объекта; 409 VERSION_CONFLICT/CHANGE_PENDING/RATE_CHANGED/COMMAND_KEY_REUSED; 422 OVERLOAD/LOSS_CONFIRMATION_REQUIRED/INVALID_TRANSITION/UNMAPPED_REFERENCE; 503 SOURCE_UNAVAILABLE/STORAGE_UNAVAILABLE. Тексты локализуются, тесты проверяют коды. Область прав применяется до агрегации и пагинации; стабильная сортировка (createdAt,id) или (name,id), cursor, ограниченный размер страниц. Подробности ошибки не раскрывают скрытые ставки.
Правила проекта и зачем
| Правило |
Зачем |
| TypeScript strict, Node 22 LTS, pnpm-монорепо и lockfile; UI — Vue 3 + Vite + vue-router без SSR, API — NestJS 12 (Express); общий eslint |
Стандартный Node-бэкенд с DI для портов; данные и контракты как в задача №215 (Drizzle, zod); UI встраивается в хост любого стека (ADR-0005); версии патчей фиксируются при старте реализации. |
| domain/ — чистый TS без декораторов Nest; контроллер = zod-DTO → фасад модуля → HTTP; Nest-модуль на каждый глубокий модуль, наружу только фасад |
Формулы и решения проверяются без Nest и браузера; DI не протекает в домен. |
| domain/ и server/ не импортируют ui/ и Vue (eslint no-restricted-imports); Calculations — отдельный пакет |
UI заменяем, формулы переиспользуются без копирования (ADR-0007). |
UI входит только через mount(el, ctx): свой экземпляр Vue, роутера и состояния на каждое монтирование, стили под корневым классом и @layer, без глобальных переменных и слушателей вне корня |
Тот же код работает в standalone-оболочке и в хосте Контура; второй экземпляр и unmount не оставляют следов (REQ-049). |
| API-клиент UI получает apiBase и авторизацию из ctx; адреса и токены не зашиты в сборку |
Standalone ходит с cookie-сессией, хост — со своим способом, экраны об этом не знают. |
| 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 не зависят от рабочих систем. |
Секреты — только @nestjs/config с проверкой env схемой zod, .env.example содержит лишь имена; в сборку UI попадает только публичная конфигурация из ctx |
Сборка клиента и репозиторий не раскрывают доступ. |
| Стабильная сортировка и типизированные ошибки |
Пагинация и автоматические проверки воспроизводимы. |
| Структурные логи + Sentry + health + UI последнего синка |
Оператор видит источник и стадию сбоя, не читая весь код. |
| ID требований в именах тестов; scripts/req-coverage; покрытие домена по testing.md |
Потерянные бизнес-требования обнаруживаются в CI. |
Будущая структура: ui/{src/{mount.ts,router,views,components,composables,api},vite.config.ts} — микрофронтенд, shell/ — standalone-оболочка (вход, шапка, шрифты), shared/contracts, server/{modules,auth,filters,composition} — NestJS (main.ts — API, worker.ts — application context), domain/{requests,assignments,calculations,reports,access,sync}, adapters/{sphere,contour,time,onec,identity,db}, packages/calculations, 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. На 15.10.2026 NestJS 12 — только ESM, Node ≥ 20, в примерах тесты на Vitest; nestjs-zod строит DTO и OpenAPI из zod-схем. Drizzle PGlite подтверждает отдельный драйвер. PGlite socket описывает single-connection ограничение: блокировки конкурентных транзакций отдельно проверяем на Postgres. Конкретные версии Vue/Vite/NestJS/Drizzle нужно закрепить lockfile и smoke в П1; текущая документация Nitro может относиться к другой основной версии, конфиг отсюда дословно не копируется.
service/docs/calculation-contract.md
Расчётный контракт дополнения
Редакция 13.10.2026. Фактический эталон — prototype-v2 commit 5213150; это контракт демо и принятых формул, а серверная архитектура утверждена 15.10.2026. Все 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С.
Ловушки реализации календаря: праздник, выпавший на выходной (09.05.2026; 01.05 и 09.05.2027), переносится на ближайший рабочий день по ст. 112 ТК; предпраздничный рабочий день на 1 ч короче (03.11.2026 — 7 ч); 31.12.2026 — выходной; в 2027 выходные 22.02, 05.11 и 31.12, а 20.02 — рабочая сокращённая суббота. Тест сверяет все 24 месячных значения таблицы, а не только годовые 1972 ч.
API принимает месяцы 2026-01…2027-12; иной месяц — 400. Форма заявки предлагает до 12 месяцев и уменьшает выбор у конца календаря, API — до 24 уникальных месяцев. Первый месяц шестимесячного плана ≤2027-07; некоммерческого отчёта с периодом 3/6/12 ≤2027-01. Недельное окно — 12 недель: неделя, включающая 2025, целиком неизвестна; норму 2025 не подставляют. Январь/февраль/март 2026 имеют 8/4/0 таких недель. Календарные даты кадрового начала могут быть старше 2026; это не добавляет часы отсутствующего календаря.
Отпуск, больничный, пересечения и полнота
reportData.vacations и reportData.sickLeaves содержат уникальные записи сотрудник×месяц. Каждая запись — одно из:
{"employee":"e1","month":"2026-10","hours":16}
{"employee":"e1","month":"2027-02","dates":["2027-02-20","2027-02-22"]}
Hours — подтверждённые рабочие часы 0…Hкал, уже без пересечений и нерабочего времени. Dates — полные дни этого месяца, не интервалы частичных часов. Повтор даты внутри записи нормализуется, неверная дата/месяц отклоняется. Объединение отпускных и больничных дат исключает повтор; праздник/выходной в dates даёт 0 ч. Пример: отпуск 20.02+22.02.2027, больничный 20.02 → исключено 7 ч, H=144.
Для пересечений оба вида вводят dates. Положительную сводку hours и датированные отсутствия одного сотрудника/месяца смешивать нельзя; API просит оба вида указать датами. Сводные часы — допущение демо: доступность каждого рабочего дня уменьшается пропорционально его календарным часам. Без дат реальное положение отсутствия в месяце неизвестно; это не прогноз больничных и не доказательство точности кадровой интеграции. Для точного дневного плана нужны dates.
Наличие записи каждого вида подтверждает полноту: пустое поле удаляет запись → unknown; hours=0 либо dates=[] подтверждает отсутствие. H неизвестна, если нет хотя бы одной из двух записей. Полная сумма отсутствий не может превышать Hкал; отказ атомарный. H = Hкал − объединённые рабочие часы отсутствий; N = сумма H выбранных сотрудников/месяцев. Hкал остаётся известной и показывается отдельно. Повторно отпуск/больничный из H/N не вычитаются; колонок с дополнительным вычетом нет. Доля каждого вида отсутствия считается от ΣHкал; общая доля — по объединению дат, а не суммой отпуска и больничного.
P, фактические часы и деньги
Обычный месячный план: P = FTE × H. Для дневного плана: P = сумма dailyFte(date) × availableDayHours(employee,date); «× 8» в FR 24 и FR 37 ТЗ — частный случай дня по 8 ч, а не отдельная формула. 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. Расчётное поведение ниже принято для прототипа; архитектура рабочего сервиса утверждена 15.10.2026, живые подключения ждут ответов open-questions.md. Общая 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 |
Расписание + ручной run |
15 мин; полная сверка сутки |
sphere:department:ID; уникальные employeeId+departmentId |
Цикл дерева/неизвестный parent → quarantine; последний валидный граф видим как stale |
Подтверждённо отсутствующее подразделение inactive; связи/история сохраняются; отчёт показывает «Не указано», если основное не выбрано |
| Сфера + утверждённое mapping → ResourceRole |
WORK_POSITION и/или согласованное UF-поле; user.fields для проверки схемы; не существует универсального REST «роль стаффинга» |
Изменение профиля или версии mapping, явный пересчёт |
При синке 15 мин; mapping по запросу РМ |
employeeId+mappingVersion; результат пересчёта не создаёт вторую роль |
Неизвестная должность → unmapped и запрет подбора до решения, не автоматическая выдача доступа |
Роль не удаляет прошлые назначения; новая роль действует для новых решений; права pm/rm отдельно |
задача №215 использует подбор роли по WORK_POSITION как подсказку. Здесь нельзя автоматически переносить его семь ролей в шесть ролей ТЗ. Mapping и многоподраздельность требуют Q-07. Для аккаунта, внезапно исчезнувшего из выборки, доступ временно приостанавливается, но увольнение не фиксируется без подтверждения. Это отличается от необратимого удаления данных.
F2. Проекты и РП — INT 01
sequenceDiagram
participant W as Sync runner
participant P as ProjectSource
participant B as Сфера
participant D as БД
W->>P: read(cursor)
P->>B: sonet_group.get ИЛИ crm.deal.list ИЛИ crm.item.list
B-->>P: ID, название, ответственный, закрытие
P-->>W: SourceProject и reference пользователя
W->>D: ExternalIdentity + Project + ProjectManager
Note over W,D: Нет подтверждённого mapping РП — нет доступа к проекту
| Источник → получатель |
Сущность и поля / метод |
Триггер |
Частота |
Ключ и идемпотентность |
Ошибка / недоступность |
Удаление / деактивация |
| Сфера: рабочая группа → ProjectSource → Project |
sonet_group.get: ID, NAME, OWNER_ID, CLOSED, PROJECT; sonet_group.user.get: USER_ID, ROLE для состава. OWNER_ID только кандидат РП, не бизнес-гарантия |
Расписание + ручная сверка |
15 мин; полная сутки |
sphere:group:ID; membership по projectId+principalId; версия mapping |
Если владелец/РП не сопоставлен, quarantine прав; старые имена можно читать, новые заявки блокировать |
CLOSED=Y закрывает новые заявки, назначения остаются; отсутствие после полной сверки → missing_pending, не hard-delete |
| Сфера: сделка → альтернативный ProjectSource |
crm.deal.list, crm.deal.fields: ID, TITLE, ASSIGNED_BY_ID, STAGE_ID, CLOSED, согласованное UF-поле РП; не любая сделка — проект |
Расписание + ручная сверка выбранной в Q-01 области |
15 мин; полная сутки |
sphere:deal:ID, sourceVersion/hash |
CRM-права могут скрывать сделки: пропуск не означает удаление; failed/stale и сверка с владельцем подключения |
Закрытие сделки трактуется по утверждённому mapping статусов проекта; история сохранена |
| Сфера: смарт-процесс → альтернативный ProjectSource |
crm.type.list → entityTypeId; crm.item.fields и crm.item.list: id, title, assignedById, stageId, согласованные custom fields |
Расписание + ручная сверка после настройки типа |
15 мин; полная сутки |
sphere:smart:entityTypeId:id; без типа ID неоднозначен |
Неизвестные поля/стадии не дают доступ автоматически; схема блокирует активацию снимка |
Терминальная стадия из mapping закрывает проект; отсутствие после полного обхода — pending confirmation |
Рекомендуем рабочие группы: задачи Time уже связаны со Сферой, tasks.task.list возвращает groupId, подход согласуется со задача №215 и не требует CRM-модели для планирования. Сделка уместна, если именно там уже ведутся проекты и РП. Смарт-процесс уместен при действующем реестре проектов с отдельным жизненным циклом; создавать его только ради интеграции не требуется. Q-01 решает сущность, признак проекта, связь РП (один/несколько) и закрытие. Любой вариант нормализуется в один Project; остальной домен не меняется.
F3. Фактические часы из Time Лоции
sequenceDiagram
participant W as Sync runner
participant T as TimeSource
participant B as SphereClient
participant D as БД
W->>T: read(window, cursor)
T-->>W: entryId, userId, taskId, date, seconds, version
W->>B: getTasks(неизвестные taskId)
B-->>W: taskId и groupId
W->>D: сопоставление сотрудника и проекта, quarantine неизвестных
W->>D: upsert entries + coverage + checkpoint
Note over W,D: Только полное покрытие позволяет вывести явный ноль
| Источник → получатель |
Сущность и поля / метод |
Триггер |
Частота |
Ключ и идемпотентность |
Ошибка / недоступность |
Удаление / деактивация |
| Time → TimeSource → ActualEntry/Coverage |
Реальный read API/export не предоставлен, Q-05. Требуем поля entryId, sphereUserId, sphereTaskId, workDate, durationSeconds, updatedAt/version, deleted, границы полного покрытия; TimeSource.read — наш порт, не метод Time |
Расписание, ручная сверка периода |
Предложение 15 мин текущий и предыдущий месяц; ночная сверка открытых периодов; закрытые по запросу |
time:entry:entryId; новая версия заменяет старую, не прибавляется; при экспорте без ID — атомарная замена полного окна, не hash как ID |
Нет схемы/права/coverage → unknown/stale; карантин сохраняет исходные строки, частичная сумма не становится полной |
Tombstone удаляет вклад записи новой версией; отсутствие считается удалением только в полном снимке окна; старый факт хранится в истории |
| Сфера → SphereClient → TaskProjectMapping |
tasks.task.list с ID-пачками до 50 как задача №215: id, groupId, responsibleId; пользователь факта берётся из Time, не из responsibleId |
По новым taskId при импорте; перепроверка mapping |
В каждом run для неизвестных; полная по окну сутки |
sphere:task:ID → projectId, version/effectiveFrom |
Несопоставленный task/group или проект иной сущности → quarantine; ручная подтверждённая связь по ID (Q-01/Q-05) |
Удаление задачи не удаляет списанные часы; перенос в группу не переписывает исторический проект без аудируемого решения |
Сумма seconds/3600 даёт A; совместимое actualFte = A/H при H>0. Исходные секунды сохраняются, перерасчёт календаря не меняет A. Агрегация employeeId×projectId×month. Нет записи ≠ 0; нули выводятся только когда источник подтверждает полное покрытие соответствующей области. Нельзя обрезать импортированный факт выше 2 H: это конфликт с FR 64, запись уходит в quarantine и Q-05, пока правило не утверждено. Отпуска не извлекаются из нерабочих задач эвристикой.
Ручной факт прототипа не суммируется с Time автоматически: предложение — Time имеет приоритет в покрытой области, ручной fallback только вне неё, correction — отдельная аудируемая запись после утверждения Q-05. Часы отпуска пока отдельные локальные записи РМ; подключение отсутствий из 1С требует отдельного подтверждённого контракта (Q-06).
F4. Календарь, отпуск, больничный и расходные ставки
sequenceDiagram
participant W as Sync runner
participant C as Calendar / Absence / Rate sources
participant D as БД снимков и полноты
participant R as Calculations / Requests
W->>C: calendar(year), absences(window), costRates
C-->>W: Версии, рабочие часы дней, отсутствия и coverage
W->>D: validate, stage, activate snapshot
R->>D: Календарь, обе записи отсутствий, C и версии
R->>R: Объединить даты, вычислить H и P, N как сумму H
R->>D: Решение и costRateSnapshot в одной транзакции
Note over D,R: Unknown не равен 0, повторного вычета отсутствий нет
Это схема предлагаемого рабочего подключения. В прототипе источники — опубликованный календарь в calc.js и ручные reportData в localStorage. Никакой sync runner, БД или 1С не запускались. Контракт отсутствий источника остаётся неизвестным, новый процесс пересогласования не вводится.
| Источник → получатель |
Сущность и поля / метод |
Триггер |
Частота |
Ключ и идемпотентность |
Ошибка / недоступность |
Удаление / деактивация |
| Опубликованный календарь → calc.js (демо); будущая 1С/утверждённый источник → CalendarVersion/Day |
Демонстрационный календарь 2026–2027, date→0/7/8 часов, праздники/переносы/рабочая суббота. Будущий endpoint неизвестен (Q-06); нужны calendarId, date, hours, version, timezone |
В демо статические данные; в сервисе предложение расписания/ручной публикации |
Предложение сутки на текущий/следующий год |
calendarId+version+date; версия целиком |
Неверные/пропущенные дни — отказ версии; за границами демо месяца — 400, недельная неделя до календаря неизвестна |
Версию не заменять отсутствующим днём как будто это выходной; политика истории вне дополнения |
| РМ → api.post report-data (демо); будущий Reports/AbsenceSource → AbsenceEntry/Coverage |
employee/month, vacations и sickLeaves; одно из hours либо dates полных дней. В будущем источник, kind, sourceVersion и coverage; endpoint 1С неизвестен |
Ручное сохранение; импорт только после получения контракта |
В демо по запросу; частота рабочего источника не утверждена |
employee+month+kind; записи уникальны; даты объединяются |
Пусто → неизвестная H; 0/пустой dates подтверждают отсутствие. Неверные даты, смешанная положительная сводка с датами, сумма >Hкал — 400 атомарно |
Удаление записи возвращает unknown; история/деактивация рабочего контура остаются проектируемыми |
| Будущая 1С/утверждённый источник → CostRateVersion |
Endpoint неизвестен (Q-04); employeeId, validFrom/To, costPerHour, currency, version и проверенное mapping |
Предложение refresh при stale перед решением |
Предложение сутки; возраст Q-03/Q-04 |
employeeId+validFrom+sourceVersion |
Нет C/валюты/mapping → unknown, рабочее approve без ставки запрещено |
Снимок согласованной C не переписывается текущим справочником; новая политика ставок не вводится |
Hкал = сумма календарных часов. H сотрудника = Hкал − объединённые рабочие часы отсутствий; P=FTE×H либо сумма дневных FTE×доступные часы дня. N=ΣH. Отпуск/больничный из N больше не вычитаются. Обе записи обязательны для известной H; календарная норма и явные фактические часы видны при неполноте. Сводные часы уже очищены от пересечений и пропорционально распределены в дневном плане; точные пересечения требуют дат обоих видов. Это допущение демо, не утверждённый формат 1С. См. полный контракт.
Источники календаря демо: 2026, 2027, переносы 2027, №1187. Сверка источников выполнена при реализации календаря 13.10.2026; региональные исключения не включены.
F5. Вход и определение роли — NFR 01
sequenceDiagram
participant U as Браузер
participant A as Access
participant I as Bitrix24 OAuth или SSO
participant D as Session и Grants
U->>A: начать вход
A-->>U: redirect + одноразовый state
U->>I: аутентификация
I-->>A: callback с code и state
A->>I: серверный обмен и проверка личности
I-->>A: подтверждённый subject
A->>D: найти Principal, active, AccessGrant
A-->>U: HttpOnly cookie или отказ доступа
U->>A: запрос бизнес API
A->>D: проверить сессию, активность и scope
| Источник → получатель |
Сущность и поля / метод |
Триггер |
Частота |
Ключ и идемпотентность |
Ошибка / недоступность |
Удаление / деактивация |
| Bitrix24 OAuth → Access → Session |
Authorize/token endpoints конкретного портала/приложения подтверждаются Q-08; после обмена user.current: ID/ACTIVE. Code flow, state, фиксированный redirectUri; секреты только server |
Начало входа и callback |
Каждый вход; refresh по expires_in при необходимости |
issuer/portal+userId → Principal; state одноразовый, повтор callback не создаёт чужую сессию |
Неверный state/issuer/redirect → отказ; OAuth недоступен → новый вход запрещён; токены/коды не логируются |
ACTIVE=false → revoke sessions; локально отозванный grant проверяется на каждом API; скрытая деактивация ограничена freshness Q-08 |
| Корпоративный SSO → альтернативный IdentityProvider |
OIDC discovery/authorize/token/userinfo по metadata согласованного issuer; sub/iss/aud/exp/nonce, PKCE S256; связи sub→sphereUserId только подтверждённые |
Вход, обновление сессии |
Каждый вход, сроки Q-08 |
issuer+sub; не email, одноразовый state/nonce |
Проверка подписи/JWKS/audience/expiry; ошибка → deny, не fallback на самозаявленный ID |
Событие revoke при наличии и периодическая сверка; удаление связи закрывает сессии, Employee остаётся |
| Серверный grant mapping → Access → scoped API |
AccessGrant principalId, role pm/rm, projectIds, viewCost; источник состава РП — утверждённый F2, РМ — назначение уполномоченным владельцем |
Каждый API-запрос; смена grants |
Без кеша права в первом релизе, источник проверяется по freshness |
principalId+grantVersion; клиент не задаёт роль |
Нет grant → 403; чужой объект → 404; неизвестная свежесть доступа после согласованного предела → deny |
Отзыв применяется к уже открытой сессии на следующем запросе, история автора сохранена |
Сессия — случайный opaque ID в Secure/HttpOnly/SameSite=Lax cookie, запись/expiry на сервере; state для OAuth хранится отдельно с коротким TTL. POST/PATCH требуют CSRF token и проверку Origin. Нельзя считать локальное приложение Bitrix с произвольным DOMAIN в callback доверенным источником: issuer/домен жёстко allowlisted. В варианте Bitrix поддержка PKCE проверяется на портале; до подтверждения не объявляется готовой. Служебный webhook для синка не идентифицирует посетителя.
F6. Заявки, назначения и мастер-данные — INT 03, NFR 02
sequenceDiagram
participant U as РП или РМ
participant A as Requests
participant P as Assignments
participant D as БД
U->>A: commandId, expectedVersion, действие
A->>A: доступ, валидация, idempotency
A->>D: BEGIN и блокировки сотрудников
A->>D: перечитать заявку, версии, ставки
A->>P: applyDecision в той же Tx
P->>P: весь дневной план, лимит FTE
P->>D: новая версия назначения
A->>D: DecisionLog + Request + CommandReceipt
A->>D: COMMIT
A-->>U: результат или типизированный конфликт
Note over A,D: Любая ошибка до COMMIT — полный ROLLBACK
| Источник → получатель |
Сущность и поля / метод |
Триггер |
Частота |
Ключ и идемпотентность |
Ошибка / недоступность |
Удаление / деактивация |
| РП/РМ → Requests → локальная БД |
/api/v1/requests, /decisions, /assignments/:id/changes; Request/ResourceLine/Assignment/Change/DecisionLog; методы и поля — architecture.md |
Явное действие с серверной авторизацией |
По запросу |
commandId+principalId и hash; версии агрегатов, уникальное pending change |
Конфликт/недоступная БД → ошибка без частичной записи; форма сохранена, повтор безопасен |
Физического удаления решения нет; вывод — новая версия дневного плана, сотрудник/проект inactive сохраняет историю |
| Сервис → Сфера |
Нет потока записи в рекомендуемом варианте; INT 03 ожидает Q-02 |
Не запускается до решения заказчика |
Отключён |
Локальные ID — мастер; внешние ID только справочников |
Недоступная Сфера не откатывает уже совершённое локальное решение; stale-политика для новых действий отдельно |
Никакие delete/update Bitrix не вызываются |
Если заказчик выбирает Bitrix мастер: заменить порт хранения Requests/Assignments, заново спроектировать атомарность решений и журнал/компенсации, пересмотреть ADR-0001 и приёмку. Это существенная развилка, не «флажок». Если нужна только публикация копии решения — отдельный outbox после локального commit, ключ eventId/version, retry и dead-letter; поле локального состояния не объявлять синхронизированным до подтверждения. В текущие пакеты запись обратно не включена.
F7. Граница с Контуром — ADR-0007
Статус: отложено до запуска Контура, в текущие пакеты не входит; живые адаптеры ждут Q-12–Q-14. Пока проекты идут потоком F2, факт — F3, ленты событий нет.
sequenceDiagram
participant K as Контур
participant A as API сервиса
participant R as Requests
participant D as БД
K->>A: POST /request-drafts (проект, строки из плана работ)
A-->>K: draftId, ссылка на форму
Note over R: РП дорабатывает и отправляет заявку, РМ решает (F6)
R->>D: решение + DecisionLog + IntegrationEvent в одной Tx
K->>A: GET /events?after=cursor
A-->>K: события по seq, nextCursor
K->>A: POST /projects/:id/release после приёмки
A->>R: pending release по каждому активному назначению
| Источник → получатель |
Сущность и поля / метод |
Триггер |
Частота |
Ключ и идемпотентность |
Ошибка / недоступность |
Удаление / деактивация |
| Контур → ProjectCatalog → Project |
externalId, name, kind, status, managerIds, ссылка на договор |
Переключение источника по Q-13, далее синк |
Как F2 |
contour:project:ID; перенос ExternalIdentity на существующий projectId |
Как F2: последний целый снимок, stale |
Закрытый проект запрещает новые заявки (REQ-029), назначения не трогает |
| Контур → API → RequestDraft |
projectId, строки: роль, сотрудник?, FTE по месяцам, R |
Кнопка «Заявка на ресурс» в карточке проекта |
По запросу |
draftId; повтор с тем же commandId возвращает тот же черновик |
4xx с кодом; план не меняется |
Черновик истекает по TTL, заявки не касается |
| Сервис → лента → Контур |
IntegrationEvent: RequestDecided, AssignmentVersionCreated, AssignmentEnded, ActualsChanged |
Решение или импорт факта |
Чтение по курсору, рекомендация — минута |
eventId + seq; at-least-once, потребитель дедуплицирует по eventId |
Недоступность потребителя не блокирует решения; лента хранится по retention Q-09 |
События не удаляются и не правятся; исправление — новое событие |
| Контур → API → release |
projectId, effectiveDate, reason, commandId |
Приёмка и закрытие проекта |
По событию Контура |
commandId; повтор не создаёт дублей |
Назначения с pending-изменением перечисляются в ответе |
Решение по каждому изменению — РМ (REQ-043) |
| Трекинг Контура → ActualsSource → ActualEntry |
Как F3: сотрудник, проект, дата, часы, ID записи |
Смена источника факта на период |
Как F3 |
contour:actual:ID; один источник на период |
Как F3 |
Как F3 |
Достоверность API и источники
Проверено 13.10.2026 по первичной документации: рабочие группы и поля, пользователь текущего токена, ограничения user scope, department API и REST 3.0, crm.item.list, поля CRM. REST v2 department.get выбран для совместимости задача №215; новые humanresources методы не обязательны для этой архитектуры.
Возможность вызова конкретного метода не доказывает наличие прав на все записи. Перед живым импортом L3 smoke на тестовом портале проверяет scope, типы ID/ACTIVE, next, доступ к неактивным, группе/проекту и полям. Боевую Сферу в этой задаче не вызывали; тестовый задача №27 не нужен для документирования неподтверждённых контрактов.
service/docs/open-questions.md
Решения заказчика
Редакция 13.10.2026 по дополнению NEW-01–09 и реализации прототипа 5213150. Расчётное поведение ниже принято для прототипа; архитектура рабочего сервиса утверждена 15.10.2026, живые подключения ждут ответов open-questions.md. Общая 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 |
Однозначные тестовые примеры на дробные часы/копейки и утверждённый способ округления |
| Q-12 / ADR-0005, ADR-0007 |
Частично решено 15.10.2026 (ответ начальника): Контур встраивает сервис микрофронтендом через Module Federation; бэкенд — NestJS. Открыто: стек хоста (Vue или другой) — от него зависит, делится ли Vue с хостом или едет в сборке remote; источник токена; общий UI-кит. |
UI уже делается микрофронтендом с mount(el, ctx) (ADR-0005), standalone — оболочка над ним; remote Module Federation с ./mount собирается с пакета 1; Vue объявлен shared singleton с запасной копией в сборке |
Сборка remote и общие зависимости (Vue singleton или своя копия); CORS/cookie или токен хоста; общий UI-кит или своя айдентика |
Записанное решение платформы: механизм, стек хоста, источник токена, UI-кит |
| Q-13 / ADR-0001, ADR-0007 |
Когда мастером проектов становится Контур и кто потребляет события: бюджет, P&L, закрытие? Нужен ли push (webhook/брокер) или достаточно ленты по курсору? Какие поля нужны бюджету? |
Лента /api/v1/events по курсору; ProjectCatalog со Сферой до запуска Контура; брокер — только при нескольких потребителях с требованием задержки |
Адаптер ProjectCatalog Контура; набор полей и версии событий REQ-042 |
Согласованный список событий и полей с потребителем, дата переключения источника проектов |
| Q-14 / NFR 01, ADR-0004, ADR-0007 |
Как роли Контура (руководитель практики, РП проекта/сервиса/продакт, финансисты, руководитель стрима и компании, клиентский менеджер, юристы, заказчик) превращаются в права сервиса? Кто видит расходные ставки? Нужны ли многосторонние согласования изменений внутри сервиса или их ведёт Контур? |
Руководитель практики → rm; РП → pm; финансисты и руководители — read-only отчёты, viewCost по Q-03; заказчик, юристы и клиентский менеджер — без доступа; дополнительные согласования ведёт Контур через DecisionPolicy |
Конфигурация сопоставления ролей; DecisionPolicy кроме «решает РМ» |
Утверждённая таблица роль → grant и правило решения по каждому виду изменения |
Что считается разрешением на следующий этап
spec.md и architecture.md утверждены 15.10.2026 — трек А разрешён. Исходное условие: утвердить spec.md с явно принятыми отклонениями от прототипа (ID/права/журнал/версии и рабочие подключения; формулы дополнения уже приняты). Q-02 определяет допустимость предложенной архитектуры в целом. Остальные неизвестные можно отложить только вместе с исключением соответствующего живого подключения из пакета; фейк не считается готовой интеграцией. После ответов обновить spec, ADR, flows и tests одной документальной ревизией, затем приступать к утверждённому пакету. Список реализации в tasks.md не создаёт задач и не назначает исполнителей автоматически.
service/docs/spec.md
Спецификация рабочего сервиса ресурсного планирования
Редакция 13.10.2026 по дополнению NEW-01–09 и реализации прототипа 5213150. Расчётное поведение ниже принято для прототипа; архитектура рабочего сервиса утверждена 15.10.2026, живые подключения ждут ответов open-questions.md. Общая QA всех NEW ещё предстоит. Расчётный контракт, матрица покрытия.
Статус: утверждено 15.10.2026 (Евгений), редакция 13.10.2026 по дополнению с правками 15.10.2026. Трек А пакетов (tasks.md) можно начинать; живые подключения трека Б ждут своих вопросов. Изменения дополнения 13.10.2026 уже включены в REQ-008/009/011/018/024/026/027 и расчётный контракт: отдельное дополнение читать не нужно, при противоречии с ТЗ v1.7 действует эта редакция, идентификаторы NEW сохранены для прослеживаемости. Принятые формулы и NEW-01–09 не требуют повторного согласования; рабочий сервис начинается после утверждения его архитектуры и относящихся вопросов подключения из tasks.md. Код в этой задаче не создаётся.
Цель и источники
РП запрашивает ресурсы; ресурсный менеджер (РМ) назначает сотрудников и принимает решения; согласованные назначения образуют единый план. Рабочий контур добавляет вход, серверные права, справочники, факт и журнал без изменения смысла формул прототипа.
База: ТЗ 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) — весь пул и решения. Технический оператор обслуживает подключения, но не получает автоматически роль РМ или расходные ставки. Должностная роль специалиста и право доступа пользователя — разные понятия.
Редакция 15.10.2026: сервис проектируется как этап 04 «Команда и ресурсы» платформы Контур (ADR-0007). В словаре Контура РМ — руководитель практики, РП — РП проекта/сервиса/продакт. Интеграцию с Контуром сейчас не делаем. В пакеты входят только дешёвые заделы, которые потом не переделать без миграций: тип объекта (REQ-039), проекты через ProjectCatalog (REQ-040), API-first (REQ-047) и UI-микрофронтенд с mount(el, ctx) (REQ-049). REQ-041–046 и REQ-048 помечены «Отложено до Контура»: описывают границу на будущее, в пакеты не входят и не проверяются до решения о запуске Контура. Формулы и жизненные циклы ниже не меняются.
Требования и проверяемые критерии
Каждая строка — отдельное требование; имя будущего теста содержит [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 |
Тестовый вход позволяет проверить обе роли без настоящего входа только в тестовом окружении; в рабочей сборке тестовый маршрут отсутствует даже при переданных флагах включения фейков. |
| REQ-039 |
Контур, этапы 02 и 05; ADR-0007 |
У проекта есть тип объекта kind = project/product/service из источника проектов; категория (REQ-025) — отдельное измерение. Отчёты и CSV фильтруют и группируют по типу; неизвестный тип показывается «Не указан», а не попадает в «Проект». |
| REQ-040 |
Контур, этап 02; ADR-0001, ADR-0007 |
Проекты приходят только через ProjectCatalog. Смена источника (Сфера → Контур) переносит ExternalIdentity на тот же локальный projectId: заявки, назначения и журнал не меняются, дубль не создаётся. Домен и отчёты не знают, какой адаптер активен. |
| REQ-041 |
Отложено до Контура; Контур, этапы 03–04; ADR-0007 |
Заявку можно открыть из внешнего контекста: POST /api/v1/request-drafts от доверенного клиента принимает projectId и необязательные строки (роль, сотрудник, FTE по месяцам, R) и возвращает ссылку на форму. Черновик — не заявка: план и журнал не меняются, РП правит и отправляет его обычным create с проверками REQ-001/002. Источник черновика сохраняется в заявке. |
| REQ-042 |
Отложено до Контура; Контур, этапы 05, 07, 10; ADR-0007 |
Решения публикуются в outbox в той же транзакции, что и журнал: RequestDecided, AssignmentVersionCreated (FTE по дням, R, снимок C), AssignmentEnded, ActualsChanged. GET /api/v1/events?after=<cursor> отдаёт ленту по возрастанию, at-least-once, с eventId, версией схемы и версией агрегата; повторное чтение не меняет состояние. Откат решения не оставляет события; C в событии — только для потребителя с viewCost. |
| REQ-043 |
Отложено до Контура; Контур, этап 10; ADR-0007 |
Внешняя команда освобождения команды проекта (POST /api/v1/projects/:id/release) создаёт pending-изменения release с указанной даты для всех активных назначений проекта, кроме уже имеющих pending-изменение (они перечисляются в ответе). План не меняется до решения РМ (REQ-016); повтор с тем же commandId не создаёт дублей. |
| REQ-044 |
Отложено до Контура; Контур, роли; NFR 01; ADR-0004, ADR-0007 |
Access принимает личность от IdentityProvider: Bitrix OAuth или доверенный шлюз платформы (подпись проверяется на сервере). Роли платформы превращаются в AccessGrant версионируемой конфигурацией (Q-14): руководитель практики → rm, РП проекта/сервиса/продакт → pm на своих проектах, прочие роли — read-only отчёты с viewCost по решению Q-03. Неизвестная роль не даёт доступа. |
| REQ-045 |
Отложено до Контура; Контур, этап 08; ADR-0007 |
Решение по заявке и изменению проходит через DecisionPolicy. По умолчанию достаточно решения РМ; если политика требует дополнительных согласований, заявка остаётся pending, план не меняется, а каждое частичное решение пишется в журнал. Отказ любого обязательного участника закрывает заявку как rejected. |
| REQ-046 |
Отложено до Контура; Контур, этап 06; ADR-0007 |
Факт приходит через ActualsSource; Time и трекинг Контура — адаптеры одного контракта с теми же правилами полноты и идемпотентности (REQ-027, REQ-035). Источник факта задаётся на период; за один период по сотруднику и проекту действует один источник, переключение не удваивает часы. |
| REQ-047 |
ADR-0005, ADR-0007 |
Каждая операция UI доступна через /api/v1; OpenAPI генерируется из shared/contracts и проверяется в CI на обратную совместимость внутри v1. domain/ и server/ не импортируют ui/ и Vue (проверка eslint). Calculations собирается отдельным пакетом и проходит L1 без Vue и NestJS. |
| REQ-049 |
ADR-0005, ADR-0007 |
UI — микрофронтенд: единственный вход mount(el, ctx) возвращает unmount(); ctx задаёт apiBase, авторизацию запросов, режим роутера (history с basePath или memory) и язык. Standalone-оболочка использует тот же mount; сборка — remote Module Federation, remoteEntry.js отдаёт ./mount, и тестовый хост на другой странице монтирует его через Module Federation. Два монтирования на одной странице не делят состояние и роутер; после unmount() в документе не остаётся узлов, стилей вне корня, слушателей и таймеров; стили микрофронтенда не меняют разметку хоста за пределами корня. |
| REQ-048 |
Отложено до Контура; Контур, этап 07 |
API отчётов принимает период датами from/to наряду с месяцем. FTE за период — среднее, взвешенное персональными нормами дней периода; неполный месяц на границе считается по дням, а не целиком. |
Жизненные циклы
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, отсутствие прав, локальное хранилище) не становятся требованиями к сервису.
service/docs/tasks.md
Пакеты реализации
Редакция 13.10.2026 по дополнению NEW-01–09 и реализации прототипа 5213150. Расчётное поведение ниже принято для прототипа; архитектура рабочего сервиса утверждена 15.10.2026, живые подключения ждут ответов open-questions.md. Общая QA всех NEW ещё предстоит. Расчётный контракт, матрица покрытия.
Эпик в Multica — задача №239 (задачи задача №240…254 = П1–П15, без исполнителей). Код живёт в этом же репозитории рядом с docs/ (/home/agents/resource-planning/service, локальный git без remote): пакет — ветка t-12345-<слаг> от main в собственном worktree (git worktree add /home/agents/resource-planning/wt/<слаг> -b t-12345-<слаг> main); основной каталог service всегда на main, в нём ветки не переключать. Вливает в main Евгений после приёмки; итог приёмки — комментарием в задаче вместо MR. Каждый пакет — одна наблюдаемая вертикаль, один MR, тест до реализации (RED → GREEN → refactor). Команды ниже будущие: появляются в П1. Общая команда приёмки каждого MR: pnpm lint && pnpm typecheck && pnpm exec tsx scripts/req-coverage; в первых MR coverage сверяется по списку REQ пакета, финальная приёмка П15 — по всем неотложенным REQ.
Не превращать пакет в горизонтальный слой «вся БД» или «весь UI». При росте объёма уточнить вертикаль до начала разработки, не делать бесконечный MR.
Конвейер
Пакет идёт сам, ручных шагов два — у Евгения. Задачу берёт сквад «Разработка»: Тимлид раздаёт, Аналитик пишет план, разработчики — код с тестами, QA и ревью проверяют, задача уходит в in_review с итоговым комментарием. План в рамках docs/ и ADR одобрен заранее; сквад останавливается только на отклонении от документов. Евгений вливает ветку в main и ставит «Готово» — это ворота стадии. Сторож multica-conveyor (таймер на сервере агентов, раз в 2 минуты) видит, что стадия закрыта, и назначает скваду задачи следующей. Задачи с пометкой «Ждёт:» (трек Б) он не запускает, пока пометку не снимут после ответа заказчика.
Два трека
Трек А — сервис на фейковых источниках (П1–П10). spec и architecture утверждены 15.10.2026 — можно начинать; вопросы к заказчику не ждёт: Сфера, Time, 1С и вход подменены fake-адаптерами и тестовой личностью. Результат — standalone-сервис, полностью проверяемый на синтетических данных.
Трек Б — живые подключения (П11–П15). Каждый пакет ждёт свои вопросы (колонка «Ворота») и служебный доступ к источнику. До ответа пакет не начинается: фикстурный адаптер без живого источника не считается интеграцией.
| Пакет / ведущая компетенция |
Вертикаль и результат одного MR |
Зависимости / ворота |
REQ |
Тесты сначала и команда приёмки |
| П1 / fullstack + QA |
Каркас: pnpm-монорепо, NestJS API (AppModule как composition root, guard доступа, exception filter, health) и worker как application context, Vitest + unplugin-swc, Vue + Vite микрофронтенд с mount(el, ctx), собранный remote Module Federation (remoteEntry.js → ./mount), тестовый хост и standalone-оболочка над ним, shared contracts (zod) и OpenAPI, PGlite/Postgres switch, Clock/ID, builders, eslint-граница domain/server ↔ ui, scripts/req-coverage. Минимальный экран после монтирования. |
Нет: spec и ADR-0005 утверждены 15.10.2026 |
037,047,049 |
L1 границы импортов; L2 миграции empty; L5 mount/unmount дважды на странице без следов, standalone smoke. pnpm test:migrations && pnpm test:e2e -- --grep REQ-049 |
| П2 / backend |
Calculations: перенос calc.js в packages/calculations на TS и Decimal — календарь, персональная H, P, деньги, средневзвешенные ставки, Σ и Итог, утилизация. Без БД, сети и Vue. |
П1; числовой контракт Q-11 (до ответа — правила из calculation-contract.md) |
008,009,010,026 |
L1 таблицы примеров calculation-contract.md и verify-docs, property-based fast-check null/0/допуски. pnpm test:domain -- --project calculations |
| П3 / backend + security |
Доступ на тестовой личности: Principal/AccessGrant/Session, scope policy pm/rm/viewCost, тестовый вход только при NODE_ENV=test, production-negative проверка. |
П1 |
012,032,038 |
L1 policy; L2 grants/session на миграциях; L4 роль × эндпоинт; prod-safety. pnpm test:module -- --project access && pnpm test:api -- --project access && pnpm test:prod-safety |
| П4 / backend + UI |
Справочники на фейке: SphereClient (fake), ProjectCatalog с типом объекта, staging/status, ExternalIdentity, деактивация, ручной run; экран справочника и ошибок синка. |
П1, П3 |
028,029,031,039,040 |
L2 два sync не дублируют, обрыв не удаляет; L3 fake pagination/errors; L4 запуск только оператором; L5 деактивация. pnpm test:module -- --project sync && pnpm test:api -- --project sync && pnpm test:e2e -- --grep REQ-029 |
| П5 / fullstack |
Заявки: РП создаёт заявку, видит scoped список и карточку; исправленная заявка — новая. |
П2–П4; Q-02 (предложение: мастер — сервис) |
001,002,006 |
L1 границы и pending; L2 create/idempotency; L4 create/get по ролям; компоненты FTE по месяцам; L5 заявка из двух строк. pnpm test:domain -- --project requests && pnpm test:api -- --project requests && pnpm test:e2e -- --grep REQ-001 |
| П6 / backend |
Решение РМ: approve/return/reject, назначения и версии, проверка ёмкости > 1 FTE с блокировками, снимок C (fake), allowLoss, журнал в одной транзакции. API без экрана плана. |
П5; Q-03/Q-04 для политики ставок (до ответа — fake source) |
003,004,005,007,030,033 |
L1 автомат; L2 rollback журнала, версии; Postgres два параллельных approve; L4 матрица решения/redaction. pnpm test:module -- --project decisions && pnpm test:postgres -- --project approval && pnpm test:api -- --project decisions |
| П7 / fullstack |
Ресурсный план: шесть месяцев, единицы FTE/ч/%, поиск и раскрытие, динамическая Σ и нижний Итог без двойного счёта, режимы РП/РМ. |
П6 |
011,012 |
L2 выборка плана по scope; компоненты таблицы плана; L5 заявка → решение → план. pnpm test:components && pnpm test:e2e -- --grep REQ-011 |
| П8 / backend + fullstack |
Изменение назначения: preview, release/reduce/replace, один pending, before/after, история, пересчёт плана и даты освобождения. |
П6–П7 |
014,015,016,017 |
L1 дневные инварианты; L2 stale snapshot; Postgres pending-race; L4 чужое назначение; L5 замена с превышением. pnpm test:domain -- --project changes && pnpm test:postgres -- --project changes && pnpm test:e2e -- --grep REQ-017 |
| П9 / fullstack + QA |
Плановые отчёты и CSV: три показателя утилизации, финансы, распределение, свободный ресурс, недельная граница календаря, scope/redaction. |
П7–П8 |
018,019,020,021 |
L1 AC 41–48; L2 выборки/суммы; L4 CSV без утечек C; L5 отчёт. pnpm test:module -- --project reports && pnpm test:api -- --project reports && pnpm test:e2e -- --grep REQ-021 |
| П10 / fullstack + backend |
Управленческие отчёты и ручные данные: некоммерческие, статистика, типы и категории, ручной факт, отпуск/больничный с полнотой обоих видов. |
П9; Q-07/Q-10 для полей (до ответа — поля прототипа) |
022,023,024,025,027 |
L1 AC 49–66, property null/0; L2 partial data; L4 PATCH только РМ; L5 полный/неполный отчёт. pnpm test:module -- --project reporting-data && pnpm test:api -- --project reporting-data && pnpm test:e2e -- --grep REQ-027 |
| П11 / integrations |
Живая Сфера: служебный read-only доступ, адаптеры сотрудников, подразделений и проектов выбранной сущности, mapping ролей, сверка с фейком из П4. |
П4; Q-01, Q-07, служебный вебхук/приложение Сферы |
028,029,031 |
L3 записи реальных ответов (captures) без секретов; L2 reconcile/quarantine; L5 стенд со снимком Сферы. pnpm test:contract -- --project sphere && pnpm test:module -- --project sync |
| П12 / integrations + QA |
Факт из Time: адаптер, raw entries, mapping задач, coverage, исправления/удаления, переключение отчётов с ручного факта, stale UI. |
П10, П11; Q-05 и captures Time |
013,031,035 |
L3 Time; L2 идемпотентность/reconcile; L4 РП не видит чужой факт; L5 outage → recovery. pnpm test:contract -- --project time && pnpm test:module -- --project actuals && pnpm test:e2e -- --grep REQ-035 |
| П13 / integrations + backend |
Календарь, отсутствия и ставки из согласованного источника: версии/coverage, короткие дни, рабочая суббота, union отсутствий, сверка rateVersion. |
П6, П10; Q-03, Q-04, Q-06, Q-11, схема 1С |
008,031,036 |
L3 1С; L2 активация версии, неизменность cost snapshot; Postgres rate/approve race; L4 RATE_CHANGED. pnpm test:contract -- --project onec && pnpm test:postgres -- --project rate-race |
| П14 / backend + security review |
Настоящий вход: Bitrix OAuth или SSO, безопасные cookies/CSRF, grant/revoke, freshness, передача портфеля по принятому правилу. |
П3, П11; Q-08 и регистрация приложения |
012,029,032,038 |
L3 identity captures; L2 sessions/revoke; L4 полная матрица роль × эндпоинт × поля; prod-safety. pnpm test:contract -- --project identity && pnpm test:api && pnpm test:prod-safety |
| П15 / platform + QA |
Эксплуатация: worker lease/fencing, расписание тех же run, статус и алерты, backup/restore runbook, нагрузочный профиль, финальная трассировка REQ. |
Все пакеты; Q-09 |
007,031,034,037 |
L2 crash/replay; Postgres fencing; L5 stale; restore и нагрузка по принятому профилю. pnpm test:postgres && pnpm test:migrations && pnpm test:e2e && pnpm test:restore && pnpm test:load && pnpm exec tsx scripts/req-coverage |
Вопрос → что разблокирует
| Вопрос |
Трек А (продолжает на фейке) |
Трек Б (ждёт) |
| Q-01 сущность проекта, Q-07 пул и роли |
П4, П10 |
П11 |
| Q-02 мастер заявок |
П5 по предложению «мастер — сервис» |
— |
| Q-03 видимость C, Q-04 источник ставок |
П6 на fake source |
П13 |
| Q-05 контракт Time |
П10 ручной факт |
П12 |
| Q-06 календарь/отсутствия, Q-11 точность |
П2 по calculation-contract.md |
П13 |
| Q-08 вход |
П3 тестовая личность |
П14 |
| Q-09 эксплуатация |
— |
П15 |
| Q-12 стек хоста, Q-13, Q-14 |
не влияют: Контур отложен |
не влияют |
П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.
Интеграция с Контуром (ADR-0007) сейчас не делается. В пакеты входят только заделы: П1 — граница импортов, OpenAPI и микрофронтенд на Module Federation (REQ-047, REQ-049), П4 — ProjectCatalog и тип объекта (REQ-039, REQ-040). Отложенные REQ-041–046 и REQ-048 станут отдельными задачами после решения о запуске Контура и ответов Q-12–Q-14.
Приёмка пакета командой
В описании MR: пользовательский результат, принятые Q/ADR, RED/GREEN команды и итог, изменение контрактов/миграций, screenshot только для изменённого UI. Reviewer прогоняет указанные команды на чистой БД, QA воспроизводит сценарий на fake-стенде, технический владелец проверяет прямой вызов модуля без браузера. Фикстурный L3 доказывает обработку заданного формата, но не работоспособность неизвестного live API. Весь сервис готов к рабочему запуску только после закрытия соответствующих вопросов и приёмки П15; никакой код не начинается только на основании этого списка.
Реализованный прототип и следующий шаг
В prototype-v2 5213150 все NEW-01–09 реализованы последовательными пакетами, матрица показывает файлы и проверки. Этот статус не выполняет П1–П15 сервиса. Фейковый выбор РМ/РП, localStorage, синхронный mock API и текущие входы отсутствий остаются границами демо. Новые процессы пересогласования/замещения/исторической фиксации денег в пакеты дополнения не добавлять. Следующая приёмка — общая QA всех NEW и риск-ревью, которые пока не завершены.
service/docs/testing.md
Проверки и требования к тестируемости
Редакция 13.10.2026 по дополнению NEW-01–09 и реализации прототипа 5213150. Расчётное поведение ниже принято для прототипа; архитектура рабочего сервиса утверждена 15.10.2026, живые подключения ждут ответов open-questions.md. Общая QA всех NEW ещё предстоит. Расчётный контракт, матрица покрытия.
Это план будущих тестов сервиса, не отчёт об их выполнении. В задача №232 проверяются документы и рендер схем; исходного кода сервиса нет. REQ из spec.md включается в имя каждого проверяющего теста, например [REQ-004] два параллельных решения не превышают дневную загрузку.
Уровни
Время — бюджет обратной связи на типичном CI runner, а не измерение и не SLA. Если превышено, искать медленные фикстуры/интеграции, не выкидывать проверки прав и расчётов.
| Уровень |
Что проверяет |
Что настоящее / что подменяется |
Инструмент |
Где и ориентир |
| L1 домен |
Формулы §1, finite-state Request/Assignment, C>R, FTE, дни изменения, null/0, округление |
Чистые функции; календарь, Clock, ID и ставки — значения/фейки; БД/сети нет |
vitest + fast-check, таблицы AC и property-based |
Локально/каждый CI, до 15 с |
| L2 модуль |
Публичные интерфейсы шести модулей, транзакции, FK/unique, idempotency, rollback, scope, снимки |
Реальная PGlite in-memory, схема из тех же миграций Drizzle; fake внешние порты |
vitest, отдельная БД на тест |
Локально/каждый CI, до 60 с |
| L3 контракт адаптеров |
Bitrix/Time/1С/IdentityProvider: parse, next/cursor, 200+error, timeout, 429, исправления, деактивация, missing fields |
Реальный HTTP-адаптер, локальный stub HTTP, записанные обезличенные ответы, без credentials |
vitest, фикстуры JSON и HTTP stub |
Локально/каждый CI, до 30 с; opt-in задача №27 отдельно до 2 мин |
| L4 API |
Коды/DTO/zod, CSRF, версия, роль×эндпоинт, field redaction, pagination |
Настоящее Nest-приложение из Test.createTestingModule с подменой портов на fake, PGlite, test identity |
vitest + unplugin-swc, supertest к app.getHttpServer() |
Локально/каждый CI, до 90 с |
| L5 e2e |
5 сценариев ниже, формы и запросы, состояние после сбоя |
Собранный test-вариант приложения, сценарные seeds, fake HTTP Sphere/Time/1С |
Playwright |
Локально/CI, до 3 мин |
| Компоненты |
Только сложная таблица плана и ввод FTE по месяцам: клавиатура, сохранение совпавших месяцев, явный 0, раскрытие |
Vue-компонент и composable; API fake; расчёты отдельно L1 |
vitest + @vue/test-utils |
Локально/CI, до 20 с |
| Миграции |
Пустая БД и snapshot предыдущей схемы с заявками/историей/частичным фактом; неизменность сумм/связей |
Реальные PGlite и Postgres, все SQL migrations |
vitest + Drizzle migrator, CI Postgres service |
CI и локально по флагу, до 90 с |
| Postgres concurrency |
Два соединения: решения одному сотруднику, два pending-change, lease/fencing, конфликт sync/rate/approval, rollback журнала |
Настоящий Postgres, те же модули; внешние порты fake |
vitest и CI Postgres service |
Каждый CI, локально по DATABASE_URL, до 60 с |
| Production safety |
Fake-login отсутствует в prod build даже с SPHERE_FAKE/TIME_FAKE, секреты отсутствуют в JS/ошибках, readiness требует Postgres |
Production build + Postgres; no live external credentials |
HTTP smoke через vitest/Node, проверка bundle |
Каждый CI, до 45 с без времени сборки |
PGlite — настоящая embedded БД для модульных тестов, но не доказательство конкурентности Postgres. Нельзя проверять race простым Promise.all в одном PGlite connection. Для Postgres тест управляет двумя соединениями и барьерами: обе транзакции стартовали, одна ждёт lock; результат ровно одно успешное решение, второе получает OVERLOAD/конфликт, а итог ≤1 FTE.
Сценарии и инварианты
L1: воспроизвести AC 41–66 из ТЗ и соответствующие createExamples из seed.js, ноябрьский пример FR 24 на производственном календаре: 0,5 FTE с 16.11.2026 → 35,5 ч при H=159 и нулевых отсутствиях. Проверить рабочую субботу 20.02.2027 на 7 ч и недельную границу января–марта 2026 без нормы 2025. Reference calc.js используется для сравнительных примеров, но тесты не должны копировать реализацию и подтверждать её ошибку.
Property-based, минимум 1000 примеров для ядра в CI, seed и минимальный counterexample печатаются при падении:
- 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 и деньги не имеют второго знаменателя продажи.
Регрессионные сценарии принятого поведения
Сценарии из приёмки дополнения 13.10.2026; числа — для календаря и персональной нормы из расчётного контракта. В сервисе они становятся L1/L5-тестами соответствующих REQ.
- 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: в списке «Расход» между «Доход» и «Статус»; суммы списка, карточки и сумма месяцев совпадают для всех заявок контрольного демо; у заявки-изменения — «—».
- NEW-03, календарь: 24 месячных значения 2026–2027; 1 FTE в ноябре 2026 = 159 ч; 20.02.2027 — рабочий день на 7 ч.
- NEW-03, норма: октябрь, отпуск 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 %.
- NEW-04: четыре верхние вкладки и пять разделов «Отчётов»; в перенесённых разделах работают свои фильтры, раскрытие и режим РП.
- NEW-05: Σ меняется с единицей (ч / FTE·мес / %); 0,5 / 1 / 0,75 FTE → 2,25; 388 ч при норме 512 ч → 75,78125 %, не сумма процентов.
- NEW-06: «Итог» = сумма строк сотрудников по месяцам и Σ; проекты повторно не прибавляются; свёртка не меняет, поиск, свободный ресурс и область РП — меняют состав.
- NEW-08: кольцо коммерческой плановой утилизации с теми же фильтрами; октябрь, 6 сотрудников, отпуск Анны 16: N = 1040, коммерческие часы 256 → 24,6 %.
- NEW-09: фактическая утилизация A / N × 100; неполный факт → «—» с пояснением; N = 0 → «—».
- «Распределение» и CSV показывают введённые ЦФО и подразделение.
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, рабочие секреты не вызывались. Публичные ссылки источников сохранены; старые источники инфраструктуры относятся к проектируемому сервису.
Производные и опубликованные страницы
Архитектура и схемы — прежний URL. Расчётный контракт, матрица NEW, analysis — читаемые HTML без CDN. Исходные Markdown/ADR также доступны из страницы схем. Статический прототип остаётся на прежнем URL, его публикация не меняется документальной редакцией.
Проверка публикации выполняется Playwright-скриптом workdir verify-published-docs.cjs: размеры/число SVG, уникальность ID и внутренние якоря, HTTP-ответы связанных страниц/Markdown, ошибки JS, viewport 1440/320/390 и скролл схем/таблиц внутри карточек. Команда прошла: 11 SVG, 20 уникальных внутренних HTTP-целей со статусом 200, уникальные ID и все проверенные якоря корректны, ошибок JS/console 0. На 1440/320/390 px переполнения страницы нет. Отдельная HTTP-проверка 18 внешних/опубликованных ссылок дала 200 для каждой; это проверка доступности, не новое подтверждение содержимого API. Снимки приложены в отчёте пакета. Старые результаты проверки архитектуры на предыдущей редакции не выдаются за новые.
Повторение сборки
В service/scripts находятся инструменты только документации, не сервер приложения. Требуются установленный mermaid-cli/marked и Chromium; окружение передаётся аргументами, новых переменных сервиса нет.
node scripts/render-docs.mjs docs /path/to/render-work /path/to/mermaid-cli/src/cli.js /path/to/puppeteer.json /path/to/marked/lib/marked.esm.js ../analysis
node scripts/verify-docs.mjs docs ../prototype-v2 ../analysis
node --check scripts/render-docs.mjs
node --check scripts/verify-docs.mjs
git diff --check
Puppeteer config содержит executablePath и args. Для изолированной среды без системного sandbox используется аргумент --no-sandbox. Выход docs/diagrams.html и docs/reference.html, analysis/reference.html — производные исходных Markdown; исходные Mermaid показаны в раскрывающихся блоках. Для обновления только прозы можно использовать последний аргумент --reuse-svg: он разрешён лишь если сохранённые Mermaid-исходники совпадают и соответствующие SVG существуют.
Граница приёмки
Формулы/NEW прототипа приняты, но серверная модель ID/Decimal/версий, безопасность, подключения, SLA и политика истории сервиса остаются предложениями и нерешёнными рабочими вопросами. Q-06 не спрашивает о формуле P или необходимости праздников/отсутствий. Дополнение не утверждает новую фиксацию денег или пересогласование. Remote сервиса отсутствует; изменения передаются локальной веткой и коммитом. Общая QA всех NEW не закрыта этим документальным пакетом.